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