1#![cfg(feature = "otlp")]
2
3use clap::ValueEnum;
12use eyre::ensure;
13use opentelemetry::{global, trace::TracerProvider, KeyValue, Value};
14use opentelemetry_otlp::{SpanExporter, WithExportConfig};
15use opentelemetry_sdk::{
16 propagation::TraceContextPropagator,
17 trace::{Sampler, SdkTracer, SdkTracerProvider},
18 Resource,
19};
20use opentelemetry_semantic_conventions::{attribute::SERVICE_VERSION, SCHEMA_URL};
21use tracing::Subscriber;
22use tracing_opentelemetry::OpenTelemetryLayer;
23use tracing_subscriber::registry::LookupSpan;
24use url::Url;
25
26const HTTP_TRACE_ENDPOINT: &str = "/v1/traces";
29const HTTP_LOGS_ENDPOINT: &str = "/v1/logs";
30
31pub fn span_layer<S>(otlp_config: OtlpConfig) -> eyre::Result<OpenTelemetryLayer<S, SdkTracer>>
36where
37 for<'span> S: Subscriber + LookupSpan<'span>,
38{
39 global::set_text_map_propagator(TraceContextPropagator::new());
40
41 let resource =
42 build_resource(otlp_config.service_name.clone(), otlp_config.service_version.as_deref());
43
44 let span_builder = SpanExporter::builder();
45
46 let span_exporter = match otlp_config.protocol {
47 OtlpProtocol::Http => {
48 span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
49 }
50 OtlpProtocol::Grpc => {
51 span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
52 }
53 };
54
55 let sampler = build_sampler(otlp_config.sample_ratio)?;
56
57 let tracer_provider = SdkTracerProvider::builder()
58 .with_resource(resource)
59 .with_sampler(sampler)
60 .with_batch_exporter(span_exporter)
61 .build();
62
63 global::set_tracer_provider(tracer_provider.clone());
64
65 let tracer = tracer_provider.tracer(otlp_config.service_name);
66 Ok(tracing_opentelemetry::layer()
67 .with_tracer(tracer)
68 .with_location(false)
69 .with_tracked_inactivity(false)
70 .with_target(false)
71 .with_threads(false))
72}
73
74#[cfg(feature = "otlp-logs")]
78pub fn log_layer(
79 otlp_config: OtlpLogsConfig,
80) -> eyre::Result<
81 opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge<
82 opentelemetry_sdk::logs::SdkLoggerProvider,
83 opentelemetry_sdk::logs::SdkLogger,
84 >,
85> {
86 use opentelemetry_otlp::LogExporter;
87 use opentelemetry_sdk::logs::SdkLoggerProvider;
88
89 let resource =
90 build_resource(otlp_config.service_name.clone(), otlp_config.service_version.as_deref());
91
92 let log_builder = LogExporter::builder();
93
94 let log_exporter = match otlp_config.protocol {
95 OtlpProtocol::Http => {
96 log_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
97 }
98 OtlpProtocol::Grpc => {
99 log_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
100 }
101 };
102
103 let logger_provider = SdkLoggerProvider::builder()
104 .with_resource(resource)
105 .with_batch_exporter(log_exporter)
106 .build();
107
108 Ok(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider))
109}
110
111#[derive(Debug, Clone)]
113pub struct OtlpConfig {
114 service_name: String,
116 service_version: Option<String>,
118 endpoint: Url,
120 protocol: OtlpProtocol,
122 sample_ratio: Option<f64>,
124}
125
126impl OtlpConfig {
127 pub fn new(
129 service_name: impl Into<String>,
130 endpoint: Url,
131 protocol: OtlpProtocol,
132 sample_ratio: Option<f64>,
133 ) -> eyre::Result<Self> {
134 if let Some(ratio) = sample_ratio {
135 ensure!(
136 (0.0..=1.0).contains(&ratio),
137 "Sample ratio must be between 0.0 and 1.0, got: {}",
138 ratio
139 );
140 }
141
142 Ok(Self {
143 service_name: service_name.into(),
144 service_version: None,
145 endpoint,
146 protocol,
147 sample_ratio,
148 })
149 }
150
151 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
153 self.service_version = Some(version.into());
154 self
155 }
156
157 pub fn service_name(&self) -> &str {
159 &self.service_name
160 }
161
162 pub const fn endpoint(&self) -> &Url {
164 &self.endpoint
165 }
166
167 pub const fn protocol(&self) -> OtlpProtocol {
169 self.protocol
170 }
171
172 pub const fn sample_ratio(&self) -> Option<f64> {
174 self.sample_ratio
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct OtlpLogsConfig {
181 service_name: String,
183 service_version: Option<String>,
185 endpoint: Url,
187 protocol: OtlpProtocol,
189}
190
191impl OtlpLogsConfig {
192 pub fn new(
194 service_name: impl Into<String>,
195 endpoint: Url,
196 protocol: OtlpProtocol,
197 ) -> eyre::Result<Self> {
198 Ok(Self { service_name: service_name.into(), service_version: None, endpoint, protocol })
199 }
200
201 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
203 self.service_version = Some(version.into());
204 self
205 }
206
207 pub fn service_name(&self) -> &str {
209 &self.service_name
210 }
211
212 pub const fn endpoint(&self) -> &Url {
214 &self.endpoint
215 }
216
217 pub const fn protocol(&self) -> OtlpProtocol {
219 self.protocol
220 }
221}
222
223fn build_resource(service_name: impl Into<Value>, service_version: Option<&str>) -> Resource {
225 let version = service_version.unwrap_or(env!("CARGO_PKG_VERSION"));
226 Resource::builder()
227 .with_service_name(service_name)
228 .with_schema_url([KeyValue::new(SERVICE_VERSION, version.to_string())], SCHEMA_URL)
229 .build()
230}
231
232fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
234 match sample_ratio {
235 None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
237 Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
239 Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
246pub enum OtlpProtocol {
247 Http,
249 Grpc,
251}
252
253impl OtlpProtocol {
254 pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
259 self.validate_endpoint_with_path(url, HTTP_TRACE_ENDPOINT)
260 }
261
262 pub fn validate_logs_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
267 self.validate_endpoint_with_path(url, HTTP_LOGS_ENDPOINT)
268 }
269
270 fn validate_endpoint_with_path(&self, url: &mut Url, http_path: &str) -> eyre::Result<()> {
271 match self {
272 Self::Http => {
273 if !url.path().ends_with(http_path) {
274 let path = url.path().trim_end_matches('/');
275 url.set_path(&format!("{}{}", path, http_path));
276 }
277 }
278 Self::Grpc => {
279 ensure!(
280 !url.path().ends_with(http_path),
281 "OTLP gRPC endpoint should not include {} path, got: {}",
282 http_path,
283 url
284 );
285 }
286 }
287 Ok(())
288 }
289}