spring_opentelemetry/
lib.rs1#![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 OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
18 OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
19 OTEL_EXPORTER_OTLP_LOGS_HEADERS,
20 OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
21 OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
23 OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
24 OTEL_EXPORTER_OTLP_METRICS_HEADERS,
25 OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
26 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
60pub 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}