1#![cfg(feature = "otlp")]
2
3use clap::ValueEnum;
10use eyre::ensure;
11use opentelemetry::{global, trace::TracerProvider, KeyValue, Value};
12use opentelemetry_otlp::{SpanExporter, WithExportConfig};
13use opentelemetry_sdk::{
14 propagation::TraceContextPropagator,
15 trace::{Sampler, SdkTracer, SdkTracerProvider},
16 Resource,
17};
18use opentelemetry_semantic_conventions::{attribute::SERVICE_VERSION, SCHEMA_URL};
19use tracing::Subscriber;
20use tracing_opentelemetry::OpenTelemetryLayer;
21use tracing_subscriber::registry::LookupSpan;
22use url::Url;
23
24const HTTP_TRACE_ENDPOINT: &str = "/v1/traces";
27
28pub fn span_layer<S>(otlp_config: OtlpConfig) -> eyre::Result<OpenTelemetryLayer<S, SdkTracer>>
33where
34 for<'span> S: Subscriber + LookupSpan<'span>,
35{
36 global::set_text_map_propagator(TraceContextPropagator::new());
37
38 let resource = build_resource(otlp_config.service_name.clone());
39
40 let span_builder = SpanExporter::builder();
41
42 let span_exporter = match otlp_config.protocol {
43 OtlpProtocol::Http => {
44 span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
45 }
46 OtlpProtocol::Grpc => {
47 span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
48 }
49 };
50
51 let sampler = build_sampler(otlp_config.sample_ratio)?;
52
53 let tracer_provider = SdkTracerProvider::builder()
54 .with_resource(resource)
55 .with_sampler(sampler)
56 .with_batch_exporter(span_exporter)
57 .build();
58
59 global::set_tracer_provider(tracer_provider.clone());
60
61 let tracer = tracer_provider.tracer(otlp_config.service_name);
62 Ok(tracing_opentelemetry::layer().with_tracer(tracer))
63}
64
65#[derive(Debug, Clone)]
67pub struct OtlpConfig {
68 service_name: String,
70 endpoint: Url,
72 protocol: OtlpProtocol,
74 sample_ratio: Option<f64>,
76}
77
78impl OtlpConfig {
79 pub fn new(
81 service_name: impl Into<String>,
82 endpoint: Url,
83 protocol: OtlpProtocol,
84 sample_ratio: Option<f64>,
85 ) -> eyre::Result<Self> {
86 if let Some(ratio) = sample_ratio {
87 ensure!(
88 (0.0..=1.0).contains(&ratio),
89 "Sample ratio must be between 0.0 and 1.0, got: {}",
90 ratio
91 );
92 }
93
94 Ok(Self { service_name: service_name.into(), endpoint, protocol, sample_ratio })
95 }
96
97 pub fn service_name(&self) -> &str {
99 &self.service_name
100 }
101
102 pub const fn endpoint(&self) -> &Url {
104 &self.endpoint
105 }
106
107 pub const fn protocol(&self) -> OtlpProtocol {
109 self.protocol
110 }
111
112 pub const fn sample_ratio(&self) -> Option<f64> {
114 self.sample_ratio
115 }
116}
117
118fn build_resource(service_name: impl Into<Value>) -> Resource {
120 Resource::builder()
121 .with_service_name(service_name)
122 .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
123 .build()
124}
125
126fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
128 match sample_ratio {
129 None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
131 Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
133 Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
140pub enum OtlpProtocol {
141 Http,
143 Grpc,
145}
146
147impl OtlpProtocol {
148 pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
153 match self {
154 Self::Http => {
155 if !url.path().ends_with(HTTP_TRACE_ENDPOINT) {
156 let path = url.path().trim_end_matches('/');
157 url.set_path(&format!("{}{}", path, HTTP_TRACE_ENDPOINT));
158 }
159 }
160 Self::Grpc => {
161 ensure!(
162 !url.path().ends_with(HTTP_TRACE_ENDPOINT),
163 "OTLP gRPC endpoint should not include {} path, got: {}",
164 HTTP_TRACE_ENDPOINT,
165 url
166 );
167 }
168 }
169 Ok(())
170 }
171}