diff --git a/src/cli/server.rs b/src/cli/server.rs
index 93888c6f..50859df4 100644
--- a/src/cli/server.rs
+++ b/src/cli/server.rs
@@ -296,15 +296,18 @@ pub(crate) async fn run_server(explicit_config_path: Option<&str>, no_browser: b
             tracing::info!("Usage logging to database enabled");
         }
 
-        // Add OTLP sink if configured
+        // Add OTLP sinks if configured
         #[cfg(feature = "otlp")]
-        if let Some(otlp_config) = &config.observability.usage.otlp
-            && otlp_config.enabled
-        {
+        use usage_sink::UsageSink as _;
+        #[cfg(feature = "otlp")]
+        for otlp_config in &config.observability.usage.otlp {
+            if !otlp_config.enabled {
+                continue;
+            }
             match usage_sink::OtlpSink::new(otlp_config, &config.observability.tracing) {
                 Ok(otlp_sink) => {
+                    tracing::info!(name = otlp_sink.name(), "Usage logging to OTLP enabled");
                     sinks.push(Arc::new(otlp_sink));
-                    tracing::info!("Usage logging to OTLP enabled");
                 }
                 Err(e) => {
                     tracing::error!(error = %e, "Failed to initialize OTLP usage sink");
@@ -312,9 +315,7 @@ pub(crate) async fn run_server(explicit_config_path: Option<&str>, no_browser: b
             }
         }
         #[cfg(not(feature = "otlp"))]
-        if let Some(otlp_config) = &config.observability.usage.otlp
-            && otlp_config.enabled
-        {
+        if config.observability.usage.otlp.iter().any(|c| c.enabled) {
             tracing::warn!(
                 "OTLP usage sink is enabled in config but the 'otlp' feature is not compiled. \
                 Rebuild with: cargo build --features otlp"
diff --git a/src/config/observability.rs b/src/config/observability.rs
index 3deb01e8..7dc1bdcf 100644
--- a/src/config/observability.rs
+++ b/src/config/observability.rs
@@ -763,10 +763,23 @@ pub struct UsageConfig {
     #[serde(default = "default_true")]
     pub database: bool,
 
-    /// OTLP exporter for usage data.
-    /// Sends usage records as OTLP log records to any OpenTelemetry-compatible backend.
+    /// OTLP exporters for usage data.
+    /// Sends usage records as OTLP log records to one or more OpenTelemetry-compatible backends.
+    /// Each entry creates an independent exporter, allowing fan-out to multiple destinations.
+    ///
+    /// ```toml
+    /// [[observability.usage.otlp]]
+    /// name = "grafana"
+    /// endpoint = "https://otlp-gateway.grafana.net/otlp"
+    /// headers = { Authorization = "Basic xxx" }
+    ///
+    /// [[observability.usage.otlp]]
+    /// name = "datadog"
+    /// endpoint = "https://otel.datadoghq.com"
+    /// headers = { "DD-API-KEY" = "xxx" }
+    /// ```
     #[serde(default)]
-    pub otlp: Option<UsageOtlpConfig>,
+    pub otlp: Vec<UsageOtlpConfig>,
 
     /// Buffer configuration for batched writes.
     #[serde(default)]
@@ -777,7 +790,7 @@ impl Default for UsageConfig {
     fn default() -> Self {
         Self {
             database: true,
-            otlp: None,
+            otlp: Vec::new(),
             buffer: UsageBufferConfig::default(),
         }
     }
@@ -792,6 +805,11 @@ pub struct UsageOtlpConfig {
     #[serde(default = "default_true")]
     pub enabled: bool,
 
+    /// Human-readable name for this endpoint (used in logs/metrics).
+    /// Defaults to the endpoint URL if not specified.
+    #[serde(default)]
+    pub name: Option<String>,
+
     /// OTLP endpoint URL.
     /// If not specified, uses the tracing OTLP endpoint.
     #[serde(default)]
diff --git a/src/usage_sink.rs b/src/usage_sink.rs
index 7512e661..928b598d 100644
--- a/src/usage_sink.rs
+++ b/src/usage_sink.rs
@@ -14,9 +14,16 @@
 //! [observability.usage]
 //! database = true  # Enable database logging (default)
 //!
-//! [observability.usage.otlp]
-//! enabled = true
-//! endpoint = "http://localhost:4317"  # or inherit from tracing.otlp
+//! # Fan out to multiple OTLP endpoints:
+//! [[observability.usage.otlp]]
+//! name = "grafana"
+//! endpoint = "https://otlp-gateway.grafana.net/otlp"
+//! headers = { Authorization = "Basic xxx" }
+//!
+//! [[observability.usage.otlp]]
+//! name = "datadog"
+//! endpoint = "https://otel.datadoghq.com"
+//! headers = { "DD-API-KEY" = "xxx" }
 //! ```
 
 use std::sync::Arc;
@@ -48,7 +55,7 @@ pub trait UsageSink: Send + Sync {
     async fn write_batch(&self, entries: &[UsageLogEntry]) -> Result<usize, UsageSinkError>;
 
     /// Get the sink name for logging/metrics.
-    fn name(&self) -> &'static str;
+    fn name(&self) -> &str;
 }
 
 /// Errors from usage sinks.
@@ -138,7 +145,7 @@ impl UsageSink for DatabaseSink {
         }
     }
 
-    fn name(&self) -> &'static str {
+    fn name(&self) -> &str {
         "database"
     }
 }
@@ -160,6 +167,7 @@ impl UsageSink for DatabaseSink {
 /// Requires the `otlp` feature.
 #[cfg(feature = "otlp")]
 pub struct OtlpSink {
+    name: String,
     logger_provider: opentelemetry_sdk::logs::SdkLoggerProvider,
     logger: opentelemetry_sdk::logs::SdkLogger,
 }
@@ -203,7 +211,14 @@ impl OtlpSink {
 
         let logger = provider.logger("hadrian.usage");
 
+        let name = config
+            .name
+            .clone()
+            .or_else(|| config.endpoint.clone())
+            .unwrap_or_else(|| "otlp".to_string());
+
         Ok(Self {
+            name,
             logger_provider: provider,
             logger,
         })
@@ -484,8 +499,8 @@ impl UsageSink for OtlpSink {
         Ok(success_count)
     }
 
-    fn name(&self) -> &'static str {
-        "otlp"
+    fn name(&self) -> &str {
+        &self.name
     }
 }
 
@@ -554,7 +569,7 @@ impl UsageSink for CompositeSink {
         }
     }
 
-    fn name(&self) -> &'static str {
+    fn name(&self) -> &str {
         "composite"
     }
 }
