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