Skip to main content

arcly_http/observability/
otel.rs

1//! OpenTelemetry OTLP tracer — init once at launch, read globally.
2//!
3//! Call `init_tracer` from `ArclyObservabilityPlugin::on_init`.
4//! Call `opentelemetry::global::shutdown_tracer_provider()` in `on_shutdown`
5//! to flush any buffered spans before the process exits.
6
7use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState};
8use opentelemetry::Context;
9
10use crate::web::context::RequestContext;
11
12/// Configuration for the OTLP span exporter.
13pub struct OtelConfig {
14    pub service_name: &'static str,
15    pub service_version: &'static str,
16    /// gRPC endpoint of the OTLP collector, e.g. `"http://localhost:4317"`.
17    pub otlp_endpoint: &'static str,
18}
19
20/// Initialise the global OpenTelemetry tracer provider with a batch OTLP
21/// gRPC exporter. Safe to call once — subsequent calls on the same process
22/// are no-ops because `set_tracer_provider` is idempotent on the global.
23pub fn init_tracer(cfg: &OtelConfig) {
24    use opentelemetry::KeyValue;
25    use opentelemetry_otlp::WithExportConfig;
26    use opentelemetry_sdk::trace::TracerProvider;
27    use opentelemetry_sdk::Resource;
28
29    let resource = Resource::new(vec![
30        KeyValue::new("service.name", cfg.service_name),
31        KeyValue::new("service.version", cfg.service_version),
32    ]);
33
34    let exporter_result = opentelemetry_otlp::SpanExporter::builder()
35        .with_tonic()
36        .with_endpoint(cfg.otlp_endpoint)
37        .build();
38
39    match exporter_result {
40        Ok(exporter) => {
41            let provider = TracerProvider::builder()
42                .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
43                .with_resource(resource)
44                .build();
45
46            opentelemetry::global::set_tracer_provider(provider);
47
48            tracing::info!(
49                endpoint = cfg.otlp_endpoint,
50                service = cfg.service_name,
51                "OTLP tracer initialised"
52            );
53        }
54        Err(e) => {
55            tracing::warn!(
56                error    = %e,
57                endpoint = cfg.otlp_endpoint,
58                "OTLP tracer failed to initialise — spans will not be exported"
59            );
60        }
61    }
62}
63
64/// Build an OpenTelemetry `Context` that represents the *parent* of the span
65/// about to be created for this request.
66///
67/// If the incoming request carried a valid W3C `traceparent` header (already
68/// parsed into `ctx.trace_id()` + `ctx.parent_span_id()`), the returned
69/// context wraps a remote `SpanContext` so the new span is linked as a child
70/// of the upstream span. For root spans (no `traceparent`), the current
71/// ambient context is returned unchanged.
72pub fn parent_context_from(ctx: &RequestContext) -> Context {
73    let parent_span_id = match ctx.parent_span_id() {
74        Some(bytes) => SpanId::from_bytes(bytes),
75        None => return Context::current(), // root span — no parent to link
76    };
77
78    let span_context = SpanContext::new(
79        TraceId::from_bytes(ctx.trace_id()),
80        parent_span_id,
81        TraceFlags::SAMPLED,
82        true, // is_remote
83        TraceState::default(),
84    );
85    Context::current().with_remote_span_context(span_context)
86}