spring_opentelemetry/
lib.rs

1//! [![spring-rs](https://img.shields.io/github/stars/spring-rs/spring-rs)](https://spring-rs.github.io/docs/plugins/spring-opentelemetry)
2#![doc(html_favicon_url = "https://spring-rs.github.io/favicon.ico")]
3#![doc(html_logo_url = "https://spring-rs.github.io/logo.svg")]
4
5pub mod metrics;
6pub mod trace;
7pub mod util;
8
9use opentelemetry_otlp::{LogExporter, MetricExporter, SpanExporter};
10#[rustfmt::skip]
11pub use opentelemetry_otlp::{
12    OTEL_EXPORTER_OTLP_COMPRESSION,
13    OTEL_EXPORTER_OTLP_ENDPOINT,
14    OTEL_EXPORTER_OTLP_HEADERS,
15    OTEL_EXPORTER_OTLP_TIMEOUT,
16    // logs
17    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
18    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
19    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
20    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
21    // metrics
22    OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
23    OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
24    OTEL_EXPORTER_OTLP_METRICS_HEADERS,
25    OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
26    // trace
27    OTEL_EXPORTER_OTLP_TRACES_COMPRESSION,
28    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
29    OTEL_EXPORTER_OTLP_TRACES_HEADERS,
30    OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
31};
32pub use opentelemetry::{global, KeyValue};
33pub use opentelemetry_sdk::Resource;
34pub use opentelemetry_semantic_conventions::resource::*;
35
36use anyhow::Context;
37use opentelemetry::propagation::TextMapCompositePropagator;
38use opentelemetry::trace::TracerProvider;
39use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
40use opentelemetry_sdk::logs::SdkLoggerProvider;
41use opentelemetry_sdk::metrics::SdkMeterProvider;
42use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
43use opentelemetry_sdk::trace::SdkTracerProvider;
44use opentelemetry_semantic_conventions::attribute;
45use schemars::JsonSchema;
46use serde::Deserialize;
47use spring::config::{ConfigRegistry, Configurable};
48use spring::plugin::component::ComponentRef;
49use spring::plugin::{ComponentRegistry, MutableComponentRegistry};
50use spring::{app::AppBuilder, error::Result, plugin::Plugin};
51use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
52
53spring::submit_config_schema!("opentelemetry", OpenTelemetryConfig);
54
55#[derive(Debug, Configurable, Clone, JsonSchema, Deserialize)]
56#[config_prefix = "opentelemetry"]
57struct OpenTelemetryConfig {
58    #[serde(default)]
59    enable: bool,
60}
61
62/// Routers collection
63pub type KeyValues = Vec<KeyValue>;
64
65pub struct OpenTelemetryPlugin;
66
67impl Plugin for OpenTelemetryPlugin {
68    fn immediately_build(&self, app: &mut AppBuilder) {
69        let config = app
70            .get_config::<OpenTelemetryConfig>()
71            .expect("opentelemetry plugin config load failed");
72        if !config.enable {
73            return;
74        }
75        let resource = Self::build_resource(app);
76        let log_provider = Self::init_logs(resource.clone());
77        let meter_provider = Self::init_metrics(resource.clone());
78        let tracer_provider = Self::init_tracer(resource);
79
80        let tracer = tracer_provider.tracer(env!("CARGO_PKG_NAME"));
81
82        let log_layer = OpenTelemetryTracingBridge::new(&log_provider);
83        let metric_layer = MetricsLayer::new(meter_provider.clone());
84        let trace_layer = OpenTelemetryLayer::new(tracer);
85
86        app.add_layer(trace_layer)
87            .add_layer(log_layer)
88            .add_layer(metric_layer)
89            .add_shutdown_hook(move |_| {
90                Box::new(Self::shutdown(
91                    tracer_provider,
92                    meter_provider,
93                    log_provider,
94                ))
95            });
96    }
97
98    fn immediately(&self) -> bool {
99        true
100    }
101}
102
103impl OpenTelemetryPlugin {
104    fn init_logs(resource: Resource) -> SdkLoggerProvider {
105        let exporter = {
106            #[cfg(feature = "http")]
107            {
108                LogExporter::builder()
109                    .with_http()
110                    .build()
111                    .expect("build http log exporter failed")
112            }
113
114            #[cfg(all(not(feature = "http"), feature = "grpc"))]
115            {
116                LogExporter::builder()
117                    .with_tonic()
118                    .build()
119                    .expect("build grpc log exporter failed")
120            }
121
122            #[cfg(not(any(feature = "http", feature = "grpc")))]
123            compile_error!(
124                "You must enable either the 'http' or 'grpc' feature for the log exporter."
125            );
126        };
127        SdkLoggerProvider::builder()
128            .with_resource(resource)
129            .with_batch_exporter(exporter)
130            .build()
131    }
132
133    fn init_metrics(resource: Resource) -> SdkMeterProvider {
134        let exporter = {
135            #[cfg(feature = "http")]
136            {
137                MetricExporter::builder()
138                    .with_http()
139                    .build()
140                    .expect("build http metric exporter failed")
141            }
142
143            #[cfg(all(not(feature = "http"), feature = "grpc"))]
144            {
145                MetricExporter::builder()
146                    .with_tonic()
147                    .build()
148                    .expect("build grpc metric exporter failed")
149            }
150
151            #[cfg(not(any(feature = "http", feature = "grpc")))]
152            compile_error!(
153                "You must enable either the 'http' or 'grpc' feature for the log exporter."
154            );
155        };
156
157        let provider = SdkMeterProvider::builder()
158            .with_resource(resource)
159            .with_periodic_exporter(exporter)
160            .build();
161
162        global::set_meter_provider(provider.clone());
163        tracing::debug!("metrics provider installed");
164
165        provider
166    }
167
168    fn init_tracer(resource: Resource) -> SdkTracerProvider {
169        let exporter = {
170            #[cfg(feature = "http")]
171            {
172                SpanExporter::builder()
173                    .with_http()
174                    .build()
175                    .expect("build http span exporter failed")
176            }
177
178            #[cfg(all(not(feature = "http"), feature = "grpc"))]
179            {
180                SpanExporter::builder()
181                    .with_tonic()
182                    .build()
183                    .expect("build grpc span exporter failed")
184            }
185
186            #[cfg(not(any(feature = "http", feature = "grpc")))]
187            compile_error!(
188                "You must enable either the 'http' or 'grpc' feature for the log exporter."
189            );
190        };
191
192        global::set_text_map_propagator(TextMapCompositePropagator::new(vec![
193            Box::new(BaggagePropagator::new()),
194            Box::new(TraceContextPropagator::new()),
195        ]));
196        #[cfg(feature = "jaeger")]
197        global::set_text_map_propagator(opentelemetry_jaeger_propagator::Propagator::new());
198        #[cfg(feature = "zipkin")]
199        global::set_text_map_propagator(opentelemetry_zipkin::Propagator::new());
200
201        let provider = SdkTracerProvider::builder()
202            .with_resource(resource)
203            .with_batch_exporter(exporter)
204            .build();
205
206        global::set_tracer_provider(provider.clone());
207        tracing::debug!("tracer provider installed");
208
209        provider
210    }
211
212    fn build_resource(app: &AppBuilder) -> Resource {
213        let mut key_values = app.get_component::<KeyValues>().unwrap_or_default();
214        key_values.push(KeyValue::new(
215            attribute::DEPLOYMENT_ENVIRONMENT_NAME,
216            format!("{:?}", app.get_env()),
217        ));
218        let mut builder = Resource::builder();
219        #[cfg(feature = "more-resource")]
220        {
221            builder = builder.with_detectors(&[
222                Box::new(opentelemetry_resource_detectors::HostResourceDetector::default()),
223                Box::new(opentelemetry_resource_detectors::OsResourceDetector),
224                Box::new(opentelemetry_resource_detectors::ProcessResourceDetector),
225            ]);
226        }
227        builder = builder.with_attributes(key_values);
228        builder.build()
229    }
230
231    async fn shutdown(
232        tracer_provider: SdkTracerProvider,
233        meter_provider: SdkMeterProvider,
234        log_provider: SdkLoggerProvider,
235    ) -> Result<String> {
236        tracer_provider
237            .shutdown()
238            .context("shutdown tracer provider failed")?;
239        meter_provider
240            .shutdown()
241            .context("shutdown meter provider failed")?;
242        log_provider
243            .shutdown()
244            .context("shutdown log provider failed")?;
245        Ok("OpenTelemetry shutdown successful".into())
246    }
247}
248
249pub trait ResourceConfigurator {
250    fn opentelemetry_attrs<KV>(&mut self, kvs: KV) -> &mut Self
251    where
252        KV: IntoIterator<Item = KeyValue>;
253}
254
255impl ResourceConfigurator for AppBuilder {
256    fn opentelemetry_attrs<KV>(&mut self, kvs: KV) -> &mut Self
257    where
258        KV: IntoIterator<Item = KeyValue>,
259    {
260        if let Some(key_values) = self.get_component_ref::<KeyValues>() {
261            unsafe {
262                let raw_ptr = ComponentRef::into_raw(key_values);
263                let key_values = &mut *(raw_ptr as *mut KeyValues);
264                key_values.extend(kvs);
265            }
266            self
267        } else {
268            let mut key_values: KeyValues = vec![];
269            key_values.extend(kvs);
270            self.add_component(key_values)
271        }
272    }
273}