Skip to main content

subcog/observability/
otlp.rs

1//! OTLP exporter configuration.
2
3use crate::config::OtlpSettings;
4
5/// OTLP transport protocol.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum OtlpProtocol {
8    /// gRPC transport (4317 default).
9    Grpc,
10    /// HTTP/protobuf transport (4318 default).
11    Http,
12}
13
14impl OtlpProtocol {
15    /// Parses protocol from environment variable value.
16    #[must_use]
17    pub fn parse(value: &str) -> Option<Self> {
18        match value.to_lowercase().as_str() {
19            "grpc" => Some(Self::Grpc),
20            "http" | "http/protobuf" | "http_binary" | "http-binary" => Some(Self::Http),
21            _ => None,
22        }
23    }
24}
25
26/// OTLP exporter configuration.
27#[derive(Debug, Clone)]
28pub struct OtlpConfig {
29    /// Collector endpoint URL.
30    pub endpoint: Option<String>,
31    /// Transport protocol.
32    pub protocol: OtlpProtocol,
33}
34
35impl OtlpConfig {
36    /// Builds OTLP configuration from environment variables.
37    #[must_use]
38    pub fn from_env() -> Self {
39        Self::from_settings(None)
40    }
41
42    /// Builds OTLP configuration from config settings with env overrides.
43    #[must_use]
44    pub fn from_settings(settings: Option<&OtlpSettings>) -> Self {
45        let protocol_explicit = settings
46            .and_then(|config| config.protocol.as_deref())
47            .is_some();
48        let endpoint = settings.and_then(|config| config.endpoint.clone());
49        let protocol = settings
50            .and_then(|config| config.protocol.as_deref())
51            .and_then(OtlpProtocol::parse)
52            .unwrap_or_else(|| protocol_from_endpoint(endpoint.as_deref()));
53
54        let mut config = Self { endpoint, protocol };
55        config.apply_env_overrides(protocol_explicit);
56        config
57    }
58}
59
60/// `OpenTelemetry` Protocol exporter.
61pub struct OtlpExporter;
62
63impl OtlpExporter {
64    /// Creates a new OTLP exporter.
65    #[must_use]
66    pub const fn new() -> Self {
67        Self
68    }
69}
70
71impl Default for OtlpExporter {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77pub(super) fn endpoint_from_env() -> Option<String> {
78    std::env::var("SUBCOG_OTLP_ENDPOINT")
79        .ok()
80        .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok())
81}
82
83fn protocol_from_env_override() -> Option<OtlpProtocol> {
84    std::env::var("SUBCOG_OTLP_PROTOCOL")
85        .or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
86        .ok()
87        .and_then(|value| OtlpProtocol::parse(&value))
88}
89
90fn protocol_from_endpoint(endpoint: Option<&str>) -> OtlpProtocol {
91    if endpoint.is_some_and(|endpoint| endpoint.contains(":4317")) {
92        return OtlpProtocol::Grpc;
93    }
94
95    OtlpProtocol::Http
96}
97
98impl OtlpConfig {
99    fn apply_env_overrides(&mut self, protocol_explicit: bool) {
100        let endpoint_override = endpoint_from_env();
101        let endpoint_overridden = endpoint_override.is_some();
102        if let Some(endpoint) = endpoint_override {
103            self.endpoint = Some(endpoint);
104        }
105
106        if let Some(protocol) = protocol_from_env_override() {
107            self.protocol = protocol;
108            return;
109        }
110
111        if endpoint_overridden && !protocol_explicit {
112            self.protocol = protocol_from_endpoint(self.endpoint.as_deref());
113        }
114    }
115}