Skip to main content

rocketmq_observability/
environment.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::ffi::OsStr;
16
17use crate::ObservabilityError;
18use crate::TelemetryBootstrapConfig;
19
20pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
21pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StandardOtlpEnvironmentStatus {
25    Unchanged,
26    Applied,
27}
28
29/// Applies the standard process-wide OTLP endpoint and protocol variables.
30///
31/// A missing or blank endpoint leaves the existing configuration untouched. A non-empty endpoint
32/// activates all three telemetry signals only when the protocol is exactly `grpc`.
33///
34/// # Errors
35///
36/// Returns [`ObservabilityError`] when a configured endpoint or protocol is not
37/// valid UTF-8, when the protocol is missing, or when it is not exactly `grpc`. Error text names
38/// only the invalid variable and never includes its value.
39pub fn apply_standard_otlp_environment(
40    config: &mut TelemetryBootstrapConfig,
41) -> Result<StandardOtlpEnvironmentStatus, ObservabilityError> {
42    let endpoint = crate::resolver::read_telemetry_environment_value(OTEL_EXPORTER_OTLP_ENDPOINT);
43    let protocol = crate::resolver::read_telemetry_environment_value(OTEL_EXPORTER_OTLP_PROTOCOL);
44    apply_standard_otlp_environment_values(config, endpoint.as_deref(), protocol.as_deref())
45}
46
47/// Applies caller-supplied standard OTLP environment values without reading process state.
48///
49/// This is the deterministic parsing boundary used by service bootstrap tests. Configuration is
50/// mutated only after both values have been validated.
51///
52/// # Errors
53///
54/// Returns [`ObservabilityError`] under the same fail-closed conditions as
55/// [`apply_standard_otlp_environment`].
56pub fn apply_standard_otlp_environment_values(
57    config: &mut TelemetryBootstrapConfig,
58    endpoint: Option<&OsStr>,
59    protocol: Option<&OsStr>,
60) -> Result<StandardOtlpEnvironmentStatus, ObservabilityError> {
61    crate::resolver::apply_standard_otlp_environment_values(config, endpoint, protocol)
62}
63
64#[cfg(test)]
65mod tests {
66    use std::ffi::OsStr;
67    #[cfg(any(unix, windows))]
68    use std::ffi::OsString;
69
70    use super::*;
71    use crate::LogsExporter;
72    use crate::MetricsExporter;
73    use crate::OtlpProtocol;
74    use crate::TraceExporter;
75
76    #[test]
77    fn missing_or_blank_endpoint_preserves_existing_configuration() {
78        let mut config = configured_bootstrap();
79        let before = snapshot(&config);
80
81        assert_eq!(
82            apply_standard_otlp_environment_values(&mut config, None, Some(OsStr::new("http/protobuf"))).unwrap(),
83            StandardOtlpEnvironmentStatus::Unchanged
84        );
85        assert_eq!(snapshot(&config), before);
86
87        assert_eq!(
88            apply_standard_otlp_environment_values(&mut config, Some(OsStr::new("  ")), Some(OsStr::new("grpc")))
89                .unwrap(),
90            StandardOtlpEnvironmentStatus::Unchanged
91        );
92        assert_eq!(snapshot(&config), before);
93    }
94
95    #[test]
96    fn exact_grpc_protocol_enables_all_otlp_signals() {
97        let mut config = configured_bootstrap();
98
99        let status = apply_standard_otlp_environment_values(
100            &mut config,
101            Some(OsStr::new(" http://collector:4317 ")),
102            Some(OsStr::new("grpc")),
103        )
104        .unwrap();
105
106        assert_eq!(status, StandardOtlpEnvironmentStatus::Applied);
107        assert!(config.observability.enabled);
108        assert!(config.observability.metrics.enabled);
109        assert_eq!(config.observability.metrics.exporter, MetricsExporter::OtlpGrpc);
110        assert!(config.observability.traces.enabled);
111        assert_eq!(config.observability.traces.exporter, TraceExporter::OtlpGrpc);
112        assert!(config.observability.logs.enabled);
113        assert_eq!(config.observability.logs.exporter, LogsExporter::OtlpGrpc);
114        assert_eq!(config.observability.otlp.endpoint, "http://collector:4317");
115        assert_eq!(config.observability.otlp.protocol, OtlpProtocol::Grpc);
116    }
117
118    #[test]
119    fn missing_or_non_grpc_protocol_fails_without_mutating_configuration() {
120        let mut config = configured_bootstrap();
121        let before = snapshot(&config);
122
123        let missing =
124            apply_standard_otlp_environment_values(&mut config, Some(OsStr::new("http://collector:4317")), None)
125                .unwrap_err();
126        assert!(!missing.to_string().contains("http://collector:4317"));
127        assert_eq!(snapshot(&config), before);
128
129        let invalid = apply_standard_otlp_environment_values(
130            &mut config,
131            Some(OsStr::new("http://user:secret@collector:4317")),
132            Some(OsStr::new("http/protobuf")),
133        )
134        .unwrap_err();
135        assert!(!invalid.to_string().contains("secret"));
136        assert_eq!(snapshot(&config), before);
137    }
138
139    #[cfg(any(unix, windows))]
140    #[test]
141    fn non_utf8_values_fail_without_mutating_configuration() {
142        let mut config = configured_bootstrap();
143        let before = snapshot(&config);
144        let invalid = invalid_os_string();
145
146        assert!(apply_standard_otlp_environment_values(
147            &mut config,
148            Some(invalid.as_os_str()),
149            Some(OsStr::new("grpc"))
150        )
151        .is_err());
152        assert_eq!(snapshot(&config), before);
153
154        assert!(apply_standard_otlp_environment_values(
155            &mut config,
156            Some(OsStr::new("http://collector:4317")),
157            Some(invalid.as_os_str())
158        )
159        .is_err());
160        assert_eq!(snapshot(&config), before);
161    }
162
163    fn configured_bootstrap() -> TelemetryBootstrapConfig {
164        let mut config = TelemetryBootstrapConfig::default();
165        config.observability.enabled = true;
166        config.observability.metrics.enabled = true;
167        config.observability.metrics.exporter = MetricsExporter::Log;
168        config.observability.traces.enabled = false;
169        config.observability.traces.exporter = TraceExporter::Disable;
170        config.observability.logs.enabled = true;
171        config.observability.logs.exporter = LogsExporter::Log;
172        config.observability.otlp.endpoint = "http://existing:4317".to_owned();
173        config
174    }
175
176    fn snapshot(
177        config: &TelemetryBootstrapConfig,
178    ) -> (
179        bool,
180        bool,
181        MetricsExporter,
182        bool,
183        TraceExporter,
184        bool,
185        LogsExporter,
186        String,
187        OtlpProtocol,
188    ) {
189        (
190            config.observability.enabled,
191            config.observability.metrics.enabled,
192            config.observability.metrics.exporter,
193            config.observability.traces.enabled,
194            config.observability.traces.exporter,
195            config.observability.logs.enabled,
196            config.observability.logs.exporter,
197            config.observability.otlp.endpoint.clone(),
198            config.observability.otlp.protocol,
199        )
200    }
201
202    #[cfg(unix)]
203    fn invalid_os_string() -> OsString {
204        use std::os::unix::ffi::OsStringExt;
205
206        OsString::from_vec(vec![0xff])
207    }
208
209    #[cfg(windows)]
210    fn invalid_os_string() -> OsString {
211        use std::os::windows::ffi::OsStringExt;
212
213        OsString::from_wide(&[0xd800])
214    }
215}