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