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 = build_resource(otlp_config.service_name.clone());
42
43 let span_builder = SpanExporter::builder();
44
45 let span_exporter = match otlp_config.protocol {
46 OtlpProtocol::Http => {
47 span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
48 }
49 OtlpProtocol::Grpc => {
50 span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
51 }
52 };
53
54 let sampler = build_sampler(otlp_config.sample_ratio)?;
55
56 let tracer_provider = SdkTracerProvider::builder()
57 .with_resource(resource)
58 .with_sampler(sampler)
59 .with_batch_exporter(span_exporter)
60 .build();
61
62 global::set_tracer_provider(tracer_provider.clone());
63
64 let tracer = tracer_provider.tracer(otlp_config.service_name);
65 Ok(tracing_opentelemetry::layer()
66 .with_tracer(tracer)
67 .with_location(false)
68 .with_tracked_inactivity(false)
69 .with_target(false)
70 .with_threads(false))
71}
72
73#[cfg(feature = "otlp-logs")]
77pub fn log_layer(
78 otlp_config: OtlpLogsConfig,
79) -> eyre::Result<
80 opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge<
81 opentelemetry_sdk::logs::SdkLoggerProvider,
82 opentelemetry_sdk::logs::SdkLogger,
83 >,
84> {
85 use opentelemetry_otlp::LogExporter;
86 use opentelemetry_sdk::logs::SdkLoggerProvider;
87
88 let resource = build_resource(otlp_config.service_name.clone());
89
90 let log_builder = LogExporter::builder();
91
92 let log_exporter = match otlp_config.protocol {
93 OtlpProtocol::Http => {
94 log_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
95 }
96 OtlpProtocol::Grpc => {
97 log_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
98 }
99 };
100
101 let logger_provider = SdkLoggerProvider::builder()
102 .with_resource(resource)
103 .with_batch_exporter(log_exporter)
104 .build();
105
106 Ok(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider))
107}
108
109#[derive(Debug, Clone)]
111pub struct OtlpConfig {
112 service_name: String,
114 endpoint: Url,
116 protocol: OtlpProtocol,
118 sample_ratio: Option<f64>,
120}
121
122impl OtlpConfig {
123 pub fn new(
125 service_name: impl Into<String>,
126 endpoint: Url,
127 protocol: OtlpProtocol,
128 sample_ratio: Option<f64>,
129 ) -> eyre::Result<Self> {
130 if let Some(ratio) = sample_ratio {
131 ensure!(
132 (0.0..=1.0).contains(&ratio),
133 "Sample ratio must be between 0.0 and 1.0, got: {}",
134 ratio
135 );
136 }
137
138 Ok(Self { service_name: service_name.into(), endpoint, protocol, sample_ratio })
139 }
140
141 pub fn service_name(&self) -> &str {
143 &self.service_name
144 }
145
146 pub const fn endpoint(&self) -> &Url {
148 &self.endpoint
149 }
150
151 pub const fn protocol(&self) -> OtlpProtocol {
153 self.protocol
154 }
155
156 pub const fn sample_ratio(&self) -> Option<f64> {
158 self.sample_ratio
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct OtlpLogsConfig {
165 service_name: String,
167 endpoint: Url,
169 protocol: OtlpProtocol,
171}
172
173impl OtlpLogsConfig {
174 pub fn new(
176 service_name: impl Into<String>,
177 endpoint: Url,
178 protocol: OtlpProtocol,
179 ) -> eyre::Result<Self> {
180 Ok(Self { service_name: service_name.into(), endpoint, protocol })
181 }
182
183 pub fn service_name(&self) -> &str {
185 &self.service_name
186 }
187
188 pub const fn endpoint(&self) -> &Url {
190 &self.endpoint
191 }
192
193 pub const fn protocol(&self) -> OtlpProtocol {
195 self.protocol
196 }
197}
198
199fn build_resource(service_name: impl Into<Value>) -> Resource {
201 Resource::builder()
202 .with_service_name(service_name)
203 .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
204 .build()
205}
206
207fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
209 match sample_ratio {
210 None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
212 Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
214 Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
221pub enum OtlpProtocol {
222 Http,
224 Grpc,
226}
227
228impl OtlpProtocol {
229 pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
234 self.validate_endpoint_with_path(url, HTTP_TRACE_ENDPOINT)
235 }
236
237 pub fn validate_logs_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
242 self.validate_endpoint_with_path(url, HTTP_LOGS_ENDPOINT)
243 }
244
245 fn validate_endpoint_with_path(&self, url: &mut Url, http_path: &str) -> eyre::Result<()> {
246 match self {
247 Self::Http => {
248 if !url.path().ends_with(http_path) {
249 let path = url.path().trim_end_matches('/');
250 url.set_path(&format!("{}{}", path, http_path));
251 }
252 }
253 Self::Grpc => {
254 ensure!(
255 !url.path().ends_with(http_path),
256 "OTLP gRPC endpoint should not include {} path, got: {}",
257 http_path,
258 url
259 );
260 }
261 }
262 Ok(())
263 }
264}