Skip to main content

lfsx_server/
telemetry.rs

1use opentelemetry::trace::TracerProvider;
2use opentelemetry_otlp::WithExportConfig;
3use opentelemetry_sdk::trace::SdkTracerProvider;
4use tracing_subscriber::EnvFilter;
5use tracing_subscriber::layer::SubscriberExt;
6use tracing_subscriber::util::SubscriberInitExt;
7
8// Traces are opt-in: `LFSX_OTLP_ENDPOINT` names the collector's HTTP traces
9// URL (`http://collector:4318/v1/traces`), and while it is unset the layer is
10// not even installed, so the operator who does not care pays nothing. Metrics
11// stay Prometheus either way; the trace answers the one question a counter
12// cannot, which is where inside a slow request the time went.
13//
14// The returned provider is the flush handle: whoever installed it shuts it
15// down on exit so the last batch of spans is not dropped on the floor.
16pub fn init() -> Option<SdkTracerProvider> {
17    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
18    let plain = tracing_subscriber::registry()
19        .with(filter)
20        .with(tracing_subscriber::fmt::layer());
21
22    let Some(endpoint) = std::env::var("LFSX_OTLP_ENDPOINT")
23        .ok()
24        .filter(|endpoint| !endpoint.is_empty())
25    else {
26        plain.init();
27        return None;
28    };
29
30    let exporter = match opentelemetry_otlp::SpanExporter::builder()
31        .with_http()
32        .with_endpoint(&endpoint)
33        .build()
34    {
35        Ok(exporter) => exporter,
36        Err(error) => {
37            plain.init();
38            tracing::error!(
39                %error,
40                endpoint,
41                "LFSX_OTLP_ENDPOINT is set but no exporter could be built for it, so traces are off"
42            );
43            return None;
44        }
45    };
46
47    opentelemetry::global::set_text_map_propagator(
48        opentelemetry_sdk::propagation::TraceContextPropagator::new(),
49    );
50
51    let provider = SdkTracerProvider::builder()
52        .with_batch_exporter(exporter)
53        .with_resource(
54            opentelemetry_sdk::Resource::builder()
55                .with_service_name("lfsx")
56                .build(),
57        )
58        .build();
59
60    plain
61        .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("lfsx")))
62        .init();
63    tracing::info!(endpoint, "traces are exported over OTLP");
64
65    Some(provider)
66}
67
68// W3C trace context onto an outbound request, so a forge or a proxy that
69// participates lands in the same trace as the request that asked. Without the
70// propagator installed (traces off) this injects nothing and costs a lookup.
71pub(crate) fn propagated(request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
72    use tracing_opentelemetry::OpenTelemetrySpanExt;
73
74    let context = tracing::Span::current().context();
75    let mut carrier = std::collections::HashMap::new();
76    opentelemetry::global::get_text_map_propagator(|propagator| {
77        propagator.inject_context(&context, &mut carrier);
78    });
79
80    carrier.into_iter().fold(request, |request, (name, value)| {
81        request.header(name, value)
82    })
83}