Skip to content

Commit db1badf

Browse files
authored
Support multiple otlp providers (#19)
* Support multiple otlp providers * Review fix
1 parent 9c7dd6c commit db1badf

3 files changed

Lines changed: 54 additions & 20 deletions

File tree

src/cli/server.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -296,25 +296,26 @@ pub(crate) async fn run_server(explicit_config_path: Option<&str>, no_browser: b
296296
tracing::info!("Usage logging to database enabled");
297297
}
298298

299-
// Add OTLP sink if configured
299+
// Add OTLP sinks if configured
300300
#[cfg(feature = "otlp")]
301-
if let Some(otlp_config) = &config.observability.usage.otlp
302-
&& otlp_config.enabled
303-
{
301+
use usage_sink::UsageSink as _;
302+
#[cfg(feature = "otlp")]
303+
for otlp_config in &config.observability.usage.otlp {
304+
if !otlp_config.enabled {
305+
continue;
306+
}
304307
match usage_sink::OtlpSink::new(otlp_config, &config.observability.tracing) {
305308
Ok(otlp_sink) => {
309+
tracing::info!(name = otlp_sink.name(), "Usage logging to OTLP enabled");
306310
sinks.push(Arc::new(otlp_sink));
307-
tracing::info!("Usage logging to OTLP enabled");
308311
}
309312
Err(e) => {
310313
tracing::error!(error = %e, "Failed to initialize OTLP usage sink");
311314
}
312315
}
313316
}
314317
#[cfg(not(feature = "otlp"))]
315-
if let Some(otlp_config) = &config.observability.usage.otlp
316-
&& otlp_config.enabled
317-
{
318+
if config.observability.usage.otlp.iter().any(|c| c.enabled) {
318319
tracing::warn!(
319320
"OTLP usage sink is enabled in config but the 'otlp' feature is not compiled. \
320321
Rebuild with: cargo build --features otlp"

src/config/observability.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -763,10 +763,23 @@ pub struct UsageConfig {
763763
#[serde(default = "default_true")]
764764
pub database: bool,
765765

766-
/// OTLP exporter for usage data.
767-
/// Sends usage records as OTLP log records to any OpenTelemetry-compatible backend.
766+
/// OTLP exporters for usage data.
767+
/// Sends usage records as OTLP log records to one or more OpenTelemetry-compatible backends.
768+
/// Each entry creates an independent exporter, allowing fan-out to multiple destinations.
769+
///
770+
/// ```toml
771+
/// [[observability.usage.otlp]]
772+
/// name = "grafana"
773+
/// endpoint = "https://otlp-gateway.grafana.net/otlp"
774+
/// headers = { Authorization = "Basic xxx" }
775+
///
776+
/// [[observability.usage.otlp]]
777+
/// name = "datadog"
778+
/// endpoint = "https://otel.datadoghq.com"
779+
/// headers = { "DD-API-KEY" = "xxx" }
780+
/// ```
768781
#[serde(default)]
769-
pub otlp: Option<UsageOtlpConfig>,
782+
pub otlp: Vec<UsageOtlpConfig>,
770783

771784
/// Buffer configuration for batched writes.
772785
#[serde(default)]
@@ -777,7 +790,7 @@ impl Default for UsageConfig {
777790
fn default() -> Self {
778791
Self {
779792
database: true,
780-
otlp: None,
793+
otlp: Vec::new(),
781794
buffer: UsageBufferConfig::default(),
782795
}
783796
}
@@ -792,6 +805,11 @@ pub struct UsageOtlpConfig {
792805
#[serde(default = "default_true")]
793806
pub enabled: bool,
794807

808+
/// Human-readable name for this endpoint (used in logs/metrics).
809+
/// Defaults to the endpoint URL if not specified.
810+
#[serde(default)]
811+
pub name: Option<String>,
812+
795813
/// OTLP endpoint URL.
796814
/// If not specified, uses the tracing OTLP endpoint.
797815
#[serde(default)]

src/usage_sink.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@
1414
//! [observability.usage]
1515
//! database = true # Enable database logging (default)
1616
//!
17-
//! [observability.usage.otlp]
18-
//! enabled = true
19-
//! endpoint = "http://localhost:4317" # or inherit from tracing.otlp
17+
//! # Fan out to multiple OTLP endpoints:
18+
//! [[observability.usage.otlp]]
19+
//! name = "grafana"
20+
//! endpoint = "https://otlp-gateway.grafana.net/otlp"
21+
//! headers = { Authorization = "Basic xxx" }
22+
//!
23+
//! [[observability.usage.otlp]]
24+
//! name = "datadog"
25+
//! endpoint = "https://otel.datadoghq.com"
26+
//! headers = { "DD-API-KEY" = "xxx" }
2027
//! ```
2128
2229
use std::sync::Arc;
@@ -48,7 +55,7 @@ pub trait UsageSink: Send + Sync {
4855
async fn write_batch(&self, entries: &[UsageLogEntry]) -> Result<usize, UsageSinkError>;
4956

5057
/// Get the sink name for logging/metrics.
51-
fn name(&self) -> &'static str;
58+
fn name(&self) -> &str;
5259
}
5360

5461
/// Errors from usage sinks.
@@ -138,7 +145,7 @@ impl UsageSink for DatabaseSink {
138145
}
139146
}
140147

141-
fn name(&self) -> &'static str {
148+
fn name(&self) -> &str {
142149
"database"
143150
}
144151
}
@@ -160,6 +167,7 @@ impl UsageSink for DatabaseSink {
160167
/// Requires the `otlp` feature.
161168
#[cfg(feature = "otlp")]
162169
pub struct OtlpSink {
170+
name: String,
163171
logger_provider: opentelemetry_sdk::logs::SdkLoggerProvider,
164172
logger: opentelemetry_sdk::logs::SdkLogger,
165173
}
@@ -203,7 +211,14 @@ impl OtlpSink {
203211

204212
let logger = provider.logger("hadrian.usage");
205213

214+
let name = config
215+
.name
216+
.clone()
217+
.or_else(|| config.endpoint.clone())
218+
.unwrap_or_else(|| "otlp".to_string());
219+
206220
Ok(Self {
221+
name,
207222
logger_provider: provider,
208223
logger,
209224
})
@@ -484,8 +499,8 @@ impl UsageSink for OtlpSink {
484499
Ok(success_count)
485500
}
486501

487-
fn name(&self) -> &'static str {
488-
"otlp"
502+
fn name(&self) -> &str {
503+
&self.name
489504
}
490505
}
491506

@@ -554,7 +569,7 @@ impl UsageSink for CompositeSink {
554569
}
555570
}
556571

557-
fn name(&self) -> &'static str {
572+
fn name(&self) -> &str {
558573
"composite"
559574
}
560575
}

0 commit comments

Comments
 (0)