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
7#[rustfmt::skip]
8pub use opentelemetry_otlp::{
9    OTEL_EXPORTER_OTLP_COMPRESSION,
10    OTEL_EXPORTER_OTLP_ENDPOINT,
11    OTEL_EXPORTER_OTLP_HEADERS,
12    OTEL_EXPORTER_OTLP_TIMEOUT,
13    // logs
14    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
15    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
16    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
17    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
18    // metrics
19    OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
20    OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
21    OTEL_EXPORTER_OTLP_METRICS_HEADERS,
22    OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
23    // trace
24    OTEL_EXPORTER_OTLP_TRACES_COMPRESSION,
25    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
26    OTEL_EXPORTER_OTLP_TRACES_HEADERS,
27    OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
28};
29pub use opentelemetry::{global, KeyValue};
30pub use opentelemetry_sdk::Resource;
31pub use opentelemetry_semantic_conventions::resource::*;
32
33use anyhow::Context;
34use opentelemetry::trace::TracerProvider;
35use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
36use opentelemetry_sdk::logs::LoggerProvider;
37use opentelemetry_sdk::metrics::SdkMeterProvider;
38use opentelemetry_sdk::propagation::TraceContextPropagator;
39use opentelemetry_sdk::trace::{self as sdktrace, BatchConfig};
40use opentelemetry_sdk::{resource, runtime};
41use opentelemetry_semantic_conventions::attribute;
42use spring::async_trait;
43use spring::config::env::Env;
44use spring::plugin::component::ComponentRef;
45use spring::plugin::{ComponentRegistry, MutableComponentRegistry};
46use spring::{app::AppBuilder, error::Result, plugin::Plugin};
47use std::time::Duration;
48use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
49
50pub struct OpenTelemetryPlugin;
51
52#[async_trait]
53impl Plugin for OpenTelemetryPlugin {
54    fn immediately_build(&self, app: &mut AppBuilder) {
55        let resource = Self::get_resource_attr(app.get_env());
56        let resource = match app.get_component_ref::<Resource>() {
57            Some(r) => resource.merge(r),
58            None => resource,
59        };
60        let log_provider = Self::init_logs(resource.clone());
61        let meter_provider = Self::init_metrics(resource.clone());
62        let tracer = Self::init_tracer(resource);
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 |_| Box::new(Self::shutdown(meter_provider, log_provider)));
72    }
73
74    fn immediately(&self) -> bool {
75        true
76    }
77}
78
79impl OpenTelemetryPlugin {
80    fn init_logs(resource: Resource) -> LoggerProvider {
81        opentelemetry_otlp::new_pipeline()
82            .logging()
83            .with_exporter(opentelemetry_otlp::new_exporter().tonic())
84            .with_resource(resource)
85            .install_batch(runtime::Tokio)
86            .expect("build LogProvider failed")
87    }
88
89    fn init_metrics(resource: Resource) -> SdkMeterProvider {
90        let provider = opentelemetry_otlp::new_pipeline()
91            .metrics(runtime::Tokio)
92            .with_exporter(opentelemetry_otlp::new_exporter().tonic())
93            .with_resource(resource)
94            .build()
95            .expect("build MeterProvider failed");
96
97        global::set_meter_provider(provider.clone());
98        tracing::debug!("metrics provider installed");
99
100        provider
101    }
102
103    fn init_tracer(resource: Resource) -> sdktrace::Tracer {
104        global::set_text_map_propagator(TraceContextPropagator::new());
105        #[cfg(feature = "jaeger")]
106        global::set_text_map_propagator(opentelemetry_jaeger_propagator::Propagator::new());
107        #[cfg(feature = "zipkin")]
108        global::set_text_map_propagator(opentelemetry_zipkin::Propagator::new());
109
110        let provider = opentelemetry_otlp::new_pipeline()
111            .tracing()
112            .with_exporter(opentelemetry_otlp::new_exporter().tonic())
113            .with_trace_config(sdktrace::Config::default().with_resource(resource))
114            .with_batch_config(BatchConfig::default())
115            .install_batch(runtime::Tokio)
116            .expect("build TraceProvider failed");
117
118        let tracer = provider.tracer(env!("CARGO_PKG_NAME"));
119        global::set_tracer_provider(provider);
120        tracing::debug!("tracer provider installed");
121
122        tracer
123    }
124
125    fn get_resource_attr(env: Env) -> Resource {
126        Self::infra_resource().merge(&Self::app_resource(env))
127    }
128
129    fn app_resource(env: Env) -> Resource {
130        Resource::from_schema_url(
131            [KeyValue::new(
132                attribute::DEPLOYMENT_ENVIRONMENT_NAME,
133                format!("{:?}", env),
134            )],
135            opentelemetry_semantic_conventions::SCHEMA_URL,
136        )
137    }
138
139    fn infra_resource() -> Resource {
140        Resource::from_detectors(
141            Duration::from_secs(0),
142            vec![
143                #[cfg(feature = "more-resource")]
144                Box::new(opentelemetry_resource_detectors::HostResourceDetector::default()),
145                #[cfg(feature = "more-resource")]
146                Box::new(opentelemetry_resource_detectors::OsResourceDetector),
147                #[cfg(feature = "more-resource")]
148                Box::new(opentelemetry_resource_detectors::ProcessResourceDetector),
149                Box::new(resource::SdkProvidedResourceDetector),
150                Box::new(resource::TelemetryResourceDetector),
151                Box::new(resource::EnvResourceDetector::new()),
152            ],
153        )
154    }
155
156    async fn shutdown(
157        meter_provider: SdkMeterProvider,
158        log_provider: LoggerProvider,
159    ) -> Result<String> {
160        global::shutdown_tracer_provider();
161        meter_provider
162            .shutdown()
163            .context("shutdown meter provider failed")?;
164        log_provider
165            .shutdown()
166            .context("shutdown log provider failed")?;
167        Ok("OpenTelemetry shutdown successful".into())
168    }
169}
170
171pub trait ResourceConfigurator {
172    fn opentelemetry_attrs<KV>(&mut self, kvs: KV) -> &mut Self
173    where
174        KV: IntoIterator<Item = KeyValue>,
175    {
176        self.merge_resource(Resource::from_schema_url(
177            kvs,
178            opentelemetry_semantic_conventions::SCHEMA_URL,
179        ))
180    }
181
182    fn merge_resource(&mut self, resource: Resource) -> &mut Self;
183}
184
185impl ResourceConfigurator for AppBuilder {
186    fn merge_resource(&mut self, resource: Resource) -> &mut Self {
187        if let Some(old_resource) = self.get_component_ref::<Resource>() {
188            unsafe {
189                let raw_ptr = ComponentRef::into_raw(old_resource) as *mut Resource;
190                let old_resource = &mut *(raw_ptr);
191                std::ptr::write(raw_ptr, old_resource.merge(&resource));
192            }
193            self
194        } else {
195            self.add_component(resource)
196        }
197    }
198}