Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cli/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct PathConfigArgs {
pub config: PathBuf,
}

fn parse_and_chdir(s: &str) -> Result<PathBuf, String> {
pub fn parse_and_chdir(s: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(s);

if !path.is_dir() {
Expand Down
66 changes: 56 additions & 10 deletions crates/cli/src/config/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use clap::{Args, ValueEnum};
use module_parser::ConfigModuleMetadata;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use std::time::Duration;

Expand All @@ -18,15 +18,19 @@ pub struct AppConfig {
/// Logging configuration.
#[serde(default = "default_logging_config")]
pub logging: LoggingConfig,
/// Tracing configuration.
/// OpenTelemetry configuration (resource, tracing, metrics).
#[serde(default)]
pub tracing: TracingConfig,
pub opentelemetry: OpenTelemetryConfig,
Comment thread
Bechma marked this conversation as resolved.
/// Directory containing per-module YAML files (optional).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modules_dir: Option<String>,
/// Per-module configuration bag: `module_name` -> module config.
#[serde(default)]
pub modules: BTreeMap<String, ModuleConfig>,
/// Per-vendor configuration bag: `vendor_name` → arbitrary JSON/YAML value.
/// Allows vendors to add their own typed configuration sections.
#[serde(default)]
pub vendor: VendorConfig,
}

impl Default for AppConfig {
Expand All @@ -35,9 +39,10 @@ impl Default for AppConfig {
server: ServerConfig::default(),
database: None,
logging: default_logging_config(),
tracing: TracingConfig::default(),
opentelemetry: OpenTelemetryConfig::default(),
modules_dir: None,
modules: BTreeMap::new(),
vendor: VendorConfig::default(),
}
}
}
Expand Down Expand Up @@ -361,27 +366,68 @@ fn parse_duration_secs(raw: &str) -> Result<Duration, String> {
.map_err(|_| format!("invalid duration seconds '{raw}'"))
}

/// Per-vendor configuration bag: vendor name → arbitrary JSON/YAML value.
/// Each vendor's section can be deserialized into a typed struct via
/// [`AppConfig::vendor_config`] or [`AppConfig::vendor_config_or_default`].
pub type VendorConfig = HashMap<String, serde_json::Value>;

/// Top-level OpenTelemetry configuration grouping resource identity,
/// a shared default exporter, tracing settings and metrics settings.
#[derive(Clone, Deserialize, Serialize, Default)]
#[serde(deny_unknown_fields)]
pub struct OpenTelemetryConfig {
#[serde(default)]
pub resource: OpenTelemetryResource,
/// Default exporter shared by tracing and metrics. Per-signal `exporter`
/// fields override this when present.
pub exporter: Option<Exporter>,
#[serde(default)]
pub tracing: TracingConfig,
#[serde(default)]
pub metrics: MetricsConfig,
Comment on lines +374 to +387
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n crates/cli/src/config/app_config.rs | sed -n '360,410p'

Repository: cyberfabric/cf-cli

Length of output: 2211


🏁 Script executed:

# Find MetricsConfig definition
rg "struct MetricsConfig" -A 20

Repository: cyberfabric/cf-cli

Length of output: 1331


🏁 Script executed:

# Find TracingConfig definition to see if it has the same pattern
rg "struct TracingConfig" -A 20

Repository: cyberfabric/cf-cli

Length of output: 1610


🏁 Script executed:

# Check for any existing fallback/inheritance logic related to exporter
rg "exporter" crates/cli/src/config/app_config.rs -B 2 -A 2

Repository: cyberfabric/cf-cli

Length of output: 962


🏁 Script executed:

# Look for any implementation code that handles exporter fallback
rg "OpenTelemetryConfig" -A 5 -B 5 | head -100

Repository: cyberfabric/cf-cli

Length of output: 2579


🏁 Script executed:

# Search for tests related to OpenTelemetry config
fd "test" --type f | xargs rg -l "OpenTelemetryConfig|MetricsConfig" 2>/dev/null | head -5

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Check if there are any impl blocks for OpenTelemetryConfig
ast-grep --pattern $'impl OpenTelemetryConfig {
  $$$
}'

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look for where these configs are actually used/instantiated
rg "metrics\.exporter|tracing\.exporter" -B 2 -A 2

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look for all test files
find . -name "*test*" -type f | grep -E "\.(rs|toml)$" | head -20

Repository: cyberfabric/cf-cli

Length of output: 102


🏁 Script executed:

# Search for test modules in the config crate
rg "#\[cfg\(test\)\]|#\[test\]" crates/cli/src/config/ -A 3

Repository: cyberfabric/cf-cli

Length of output: 1871


🏁 Script executed:

# Look for where OpenTelemetry setup happens (likely in a separate init/setup file)
fd -e rs | xargs rg -l "TracingConfig|MetricsConfig" | grep -v app_config

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Check the actual usage of these configs - look for where metrics exporter is extracted/used
rg "\.metrics" crates/cli/src/ -B 2 -A 2 | head -60

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Search for where telemetry is actually initialized or used
rg "otel|telemetry|instrumentation" crates/cli/src/ -l | head -10

Repository: cyberfabric/cf-cli

Length of output: 207


🏁 Script executed:

# Look for any mention of metrics or tracing initialization
rg "metrics_config|tracing_config|otel_config" crates/cli/src/ -B 2 -A 2 | head -80

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Check if there's a separate module for telemetry setup
find crates/cli/src -name "*.rs" | xargs grep -l "metrics\|tracing" | grep -v ".git"

Repository: cyberfabric/cf-cli

Length of output: 122


🏁 Script executed:

# Search more broadly for where the config fields are accessed
rg "\.exporter" crates/cli/src/ -B 3 -A 3

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look at run_loop.rs to see how telemetry config is used
cat -n crates/cli/src/run/run_loop.rs | head -150

Repository: cyberfabric/cf-cli

Length of output: 7688


🏁 Script executed:

# Check for any exporter resolution or fallback logic
rg "opentelemetry.exporter|otel.*exporter" crates/ -i

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look at how metrics config is actually used in run_loop
rg "metrics" crates/cli/src/run/run_loop.rs -B 3 -A 3

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Search for any function that might handle the exporter inheritance
rg "fn.*exporter|exporter.*fn" crates/cli/src/ -i -B 1 -A 5

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Check common.rs for telemetry/config setup
cat -n crates/cli/src/common.rs | head -200

Repository: cyberfabric/cf-cli

Length of output: 7135


🏁 Script executed:

# Search for where metrics exporter is actually instantiated or used
rg "MetricsConfig" crates/cli/src/ -B 5 -A 5

Repository: cyberfabric/cf-cli

Length of output: 1544


🏁 Script executed:

# Look for any builder or post-processing after deserialization
rg "from_config|with_exporter|exporter.*=|metrics.*=" crates/cli/src/ -i

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look for modkit usage - this is where the config is actually applied
rg "run_server|AppConfig" crates/cli/src/ -B 2 -A 5

Repository: cyberfabric/cf-cli

Length of output: 7888


🏁 Script executed:

# Check if there's a specific handler for exporter in metrics setup
rg "impl.*MetricsConfig|fn.*metrics" crates/cli/src/ -B 2 -A 8

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Look for where Exporter::default is used or referenced
rg "Exporter::default|Default for.*Exporter" crates/cli/src/

Repository: cyberfabric/cf-cli

Length of output: 44


🏁 Script executed:

# Check the Exporter struct more carefully
rg "struct Exporter" -A 15 crates/cli/src/config/

Repository: cyberfabric/cf-cli

Length of output: 1172


Change MetricsConfig.exporter to Option<Exporter> to inherit the shared default exporter.

MetricsConfig.exporter is currently a concrete Exporter type with #[serde(default)], which prevents the field from distinguishing "unset, inherit the shared exporter" from "explicitly configured as default". After deserialization, metrics always has Exporter::default(), breaking the documented design that per-signal fields can override opentelemetry.exporter when present.

This differs from TracingConfig.exporter, which correctly uses Option<Exporter> to support inheritance. A config that only sets opentelemetry.exporter.kind will therefore lose that shared kind for metrics.

Shape change
 pub struct MetricsConfig {
     #[serde(default)]
     pub enabled: bool,
-    #[serde(default)]
-    pub exporter: Exporter,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub exporter: Option<Exporter>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub cardinality_limit: Option<usize>,
 }

Resolve the effective exporter after deserialization by falling back to OpenTelemetryConfig.exporter when metrics.exporter is None. Add a config test with only opentelemetry.exporter populated to prevent regression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/cli/src/config/app_config.rs` around lines 374 - 387, Change
MetricsConfig.exporter from a concrete Exporter with #[serde(default)] to
Option<Exporter> (mirroring TracingConfig.exporter) so metrics can distinguish
"unset" vs "explicit default"; update deserialization/merge logic in
OpenTelemetryConfig (or wherever effective exporter is resolved) to fall back to
OpenTelemetryConfig.exporter when metrics.exporter is None; remove the
inappropriate #[serde(default)] on the metrics exporter field and add a
unit/config test that only sets opentelemetry.exporter.kind to verify metrics
inherits the shared exporter.

}

/// OpenTelemetry resource identity — attached to all traces and metrics.
#[derive(Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OpenTelemetryResource {
/// Logical service name.
#[serde(default = "default_service_name")]
pub service_name: String,
/// Extra resource attributes added to every span and metric data point.
#[serde(default)]
pub attributes: BTreeMap<String, String>,
}

/// Return the default OpenTelemetry service name used when none is configured.
fn default_service_name() -> String {
"cyberfabric".to_owned()
}

impl Default for OpenTelemetryResource {
fn default() -> Self {
Self {
service_name: default_service_name(),
attributes: BTreeMap::default(),
}
}
}

/// Tracing configuration for OpenTelemetry distributed tracing.
#[derive(Clone, Deserialize, Serialize, Default)]
pub struct TracingConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exporter: Option<Exporter>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sampler: Option<Sampler>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub propagation: Option<Propagation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub resource: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub http: Option<HttpOpts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logs_correlation: Option<LogsCorrelation>,
#[serde(default)]
pub metrics: MetricsConfig,
}

/// Metrics configuration for OpenTelemetry metrics collection.
Expand Down