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