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().with_tracer(tracer))
66}
67
68#[cfg(feature = "otlp-logs")]
72pub fn log_layer(
73 otlp_config: OtlpLogsConfig,
74) -> eyre::Result<
75 opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge<
76 opentelemetry_sdk::logs::SdkLoggerProvider,
77 opentelemetry_sdk::logs::SdkLogger,
78 >,
79> {
80 use opentelemetry_otlp::LogExporter;
81 use opentelemetry_sdk::logs::SdkLoggerProvider;
82
83 let resource = build_resource(otlp_config.service_name.clone());
84
85 let log_builder = LogExporter::builder();
86
87 let log_exporter = match otlp_config.protocol {
88 OtlpProtocol::Http => {
89 log_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
90 }
91 OtlpProtocol::Grpc => {
92 log_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
93 }
94 };
95
96 let logger_provider = SdkLoggerProvider::builder()
97 .with_resource(resource)
98 .with_batch_exporter(log_exporter)
99 .build();
100
101 Ok(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider))
102}
103
104#[derive(Debug, Clone)]
106pub struct OtlpConfig {
107 service_name: String,
109 endpoint: Url,
111 protocol: OtlpProtocol,
113 sample_ratio: Option<f64>,
115}
116
117impl OtlpConfig {
118 pub fn new(
120 service_name: impl Into<String>,
121 endpoint: Url,
122 protocol: OtlpProtocol,
123 sample_ratio: Option<f64>,
124 ) -> eyre::Result<Self> {
125 if let Some(ratio) = sample_ratio {
126 ensure!(
127 (0.0..=1.0).contains(&ratio),
128 "Sample ratio must be between 0.0 and 1.0, got: {}",
129 ratio
130 );
131 }
132
133 Ok(Self { service_name: service_name.into(), endpoint, protocol, sample_ratio })
134 }
135
136 pub fn service_name(&self) -> &str {
138 &self.service_name
139 }
140
141 pub const fn endpoint(&self) -> &Url {
143 &self.endpoint
144 }
145
146 pub const fn protocol(&self) -> OtlpProtocol {
148 self.protocol
149 }
150
151 pub const fn sample_ratio(&self) -> Option<f64> {
153 self.sample_ratio
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct OtlpLogsConfig {
160 service_name: String,
162 endpoint: Url,
164 protocol: OtlpProtocol,
166}
167
168impl OtlpLogsConfig {
169 pub fn new(
171 service_name: impl Into<String>,
172 endpoint: Url,
173 protocol: OtlpProtocol,
174 ) -> eyre::Result<Self> {
175 Ok(Self { service_name: service_name.into(), endpoint, protocol })
176 }
177
178 pub fn service_name(&self) -> &str {
180 &self.service_name
181 }
182
183 pub const fn endpoint(&self) -> &Url {
185 &self.endpoint
186 }
187
188 pub const fn protocol(&self) -> OtlpProtocol {
190 self.protocol
191 }
192}
193
194fn build_resource(service_name: impl Into<Value>) -> Resource {
196 Resource::builder()
197 .with_service_name(service_name)
198 .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
199 .build()
200}
201
202fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
204 match sample_ratio {
205 None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
207 Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
209 Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
216pub enum OtlpProtocol {
217 Http,
219 Grpc,
221}
222
223impl OtlpProtocol {
224 pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
229 self.validate_endpoint_with_path(url, HTTP_TRACE_ENDPOINT)
230 }
231
232 pub fn validate_logs_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
237 self.validate_endpoint_with_path(url, HTTP_LOGS_ENDPOINT)
238 }
239
240 fn validate_endpoint_with_path(&self, url: &mut Url, http_path: &str) -> eyre::Result<()> {
241 match self {
242 Self::Http => {
243 if !url.path().ends_with(http_path) {
244 let path = url.path().trim_end_matches('/');
245 url.set_path(&format!("{}{}", path, http_path));
246 }
247 }
248 Self::Grpc => {
249 ensure!(
250 !url.path().ends_with(http_path),
251 "OTLP gRPC endpoint should not include {} path, got: {}",
252 http_path,
253 url
254 );
255 }
256 }
257 Ok(())
258 }
259}