mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-24 04:14:08 +00:00
Implement the Jaeger trace propagator in-tree
The `opentelemetry-jaeger-propagator` crate was removed upstream after 0.32 (open-telemetry/opentelemetry-rust#3710), but Synapse propagates traces with the Jaeger `uber-trace-id` header, so MAS needs to keep supporting it. The in-tree propagator only handles that header, including the URL-encoded form jaeger-client-python sends and the debug flag set by Synapse's forced tracing. Baggage (`uberctx-*` headers) is not propagated. A missing or malformed header is ignored without logging, so the filter for the upstream crate's "invalid header" warning goes away too.
This commit is contained in:
6 files changed
+302
-29
No files matched your search
Generated
-10
@@ -3395,7 +3395,6 @@ dependencies = [
|
||||
"opentelemetry-http",
|
||||
"opentelemetry-instrumentation-process",
|
||||
"opentelemetry-instrumentation-tokio",
|
||||
"opentelemetry-jaeger-propagator",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry-prometheus-text-exporter",
|
||||
"opentelemetry-resource-detectors",
|
||||
@@ -4383,15 +4382,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-jaeger-propagator"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92c2cebef9a57a493394ba0901b692a6915cc4a1314e93f905db1344392b4aa4"
|
||||
dependencies = [
|
||||
"opentelemetry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-otlp"
|
||||
version = "0.32.0"
|
||||
|
||||
+1
-2
@@ -1,3 +1,4 @@
|
||||
# Copyright 2025, 2026 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -410,8 +411,6 @@ features = ["reqwest"]
|
||||
version = "0.2.0"
|
||||
[workspace.dependencies.opentelemetry-instrumentation-tokio]
|
||||
version = "0.2.0"
|
||||
[workspace.dependencies.opentelemetry-jaeger-propagator]
|
||||
version = "0.32.0"
|
||||
[workspace.dependencies.opentelemetry-otlp]
|
||||
version = "0.32.0"
|
||||
default-features = false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Copyright 2025, 2026 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -59,7 +60,6 @@ opentelemetry.workspace = true
|
||||
opentelemetry-http.workspace = true
|
||||
opentelemetry-instrumentation-process.workspace = true
|
||||
opentelemetry-instrumentation-tokio.workspace = true
|
||||
opentelemetry-jaeger-propagator.workspace = true
|
||||
opentelemetry-otlp.workspace = true
|
||||
opentelemetry-prometheus-text-exporter.workspace = true
|
||||
opentelemetry-resource-detectors.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -13,10 +14,7 @@ use clap::Parser;
|
||||
use mas_config::{ConfigurationSectionExt, TelemetryConfig};
|
||||
use sentry_tracing::EventFilter;
|
||||
use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
filter::{LevelFilter, filter_fn},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer, Registry, filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt,
|
||||
};
|
||||
|
||||
mod app_state;
|
||||
@@ -104,10 +102,6 @@ async fn try_main() -> anyhow::Result<ExitCode> {
|
||||
.or_else(|_| EnvFilter::try_new("info"))
|
||||
.context("could not setup logging filter")?;
|
||||
|
||||
// Suppress the following warning from the Jaeger propagator:
|
||||
// Invalid jaeger header format header_value=""
|
||||
let suppress_layer = filter_fn(|metadata| metadata.name() != "JaegerPropagator.InvalidHeader");
|
||||
|
||||
// Setup the rustls crypto provider
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
@@ -163,7 +157,6 @@ async fn try_main() -> anyhow::Result<ExitCode> {
|
||||
.with_filter(LevelFilter::INFO);
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(suppress_layer)
|
||||
.with(sentry_layer)
|
||||
.with(telemetry_layer)
|
||||
.with(filter_layer)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -33,6 +34,8 @@ use opentelemetry_sdk::{
|
||||
};
|
||||
use opentelemetry_semantic_conventions as semcov;
|
||||
|
||||
mod jaeger;
|
||||
|
||||
static SCOPE: LazyLock<InstrumentationScope> = LazyLock::new(|| {
|
||||
InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
@@ -83,13 +86,7 @@ fn match_propagator(propagator: Propagator) -> Box<dyn TextMapPropagator + Send
|
||||
match propagator {
|
||||
P::TraceContext => Box::new(TraceContextPropagator::new()),
|
||||
P::Baggage => Box::new(BaggagePropagator::new()),
|
||||
// opentelemetry-jaeger-propagator is deprecated upstream but still
|
||||
// published; we keep supporting the Jaeger format until it's removed.
|
||||
#[expect(
|
||||
deprecated,
|
||||
reason = "the Jaeger propagator is deprecated upstream but we still expose it as an option"
|
||||
)]
|
||||
P::Jaeger => Box::new(opentelemetry_jaeger_propagator::Propagator::new()),
|
||||
P::Jaeger => Box::new(jaeger::JaegerPropagator::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
//! A minimal propagator for the Jaeger `uber-trace-id` header, the trace
|
||||
//! context format Synapse uses.
|
||||
//!
|
||||
//! The header value is `{trace-id}:{span-id}:{parent-span-id}:{flags}`, all
|
||||
//! hex-encoded, as described in
|
||||
//! <https://www.jaegertracing.io/sdk-migration/#propagation-format>.
|
||||
//! The parent span ID is deprecated in that format, so it is ignored on
|
||||
//! extraction and sent as `0`. Baggage (`uberctx-*` headers) is not
|
||||
//! propagated.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use opentelemetry::{
|
||||
Context,
|
||||
propagation::{Extractor, Injector, TextMapPropagator, text_map_propagator::FieldIter},
|
||||
trace::{SpanContext, SpanId, TraceContextExt as _, TraceFlags, TraceId, TraceState},
|
||||
};
|
||||
|
||||
const HEADER_NAME: &str = "uber-trace-id";
|
||||
|
||||
/// Jaeger's debug flag has no W3C equivalent, so it is carried in a bit that
|
||||
/// the W3C propagator masks out.
|
||||
const TRACE_FLAG_DEBUG: TraceFlags = TraceFlags::new(0x04);
|
||||
|
||||
/// Propagates the trace context through the Jaeger `uber-trace-id` header
|
||||
#[derive(Debug)]
|
||||
pub struct JaegerPropagator {
|
||||
fields: [String; 1],
|
||||
}
|
||||
|
||||
impl JaegerPropagator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fields: [HEADER_NAME.to_owned()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the value of an `uber-trace-id` header
|
||||
fn parse_header(value: &str) -> Option<SpanContext> {
|
||||
// jaeger-client-python, which Synapse uses, URL-encodes the whole value
|
||||
let value = if value.contains(':') {
|
||||
Cow::Borrowed(value)
|
||||
} else {
|
||||
Cow::Owned(value.replace("%3A", ":").replace("%3a", ":"))
|
||||
};
|
||||
|
||||
let mut parts = value.split(':');
|
||||
let trace_id = parts.next()?;
|
||||
let span_id = parts.next()?;
|
||||
let _parent_span_id = parts.next()?;
|
||||
let flags = parts.next()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Jaeger clients drop leading zeros from the IDs, so shorter values are valid
|
||||
if trace_id.is_empty() || trace_id.len() > 32 {
|
||||
return None;
|
||||
}
|
||||
if span_id.is_empty() || span_id.len() > 16 {
|
||||
return None;
|
||||
}
|
||||
if flags.is_empty() || flags.len() > 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let trace_id = TraceId::from_hex(trace_id).ok()?;
|
||||
let span_id = SpanId::from_hex(span_id).ok()?;
|
||||
let flags = u8::from_str_radix(flags, 16).ok()?;
|
||||
// The debug flag (0x02) only has an effect on sampled traces
|
||||
let trace_flags = match flags & 0x03 {
|
||||
0x03 => TraceFlags::SAMPLED | TRACE_FLAG_DEBUG,
|
||||
0x01 => TraceFlags::SAMPLED,
|
||||
_ => TraceFlags::default(),
|
||||
};
|
||||
|
||||
let span_context = SpanContext::new(trace_id, span_id, trace_flags, true, TraceState::NONE);
|
||||
span_context.is_valid().then_some(span_context)
|
||||
}
|
||||
|
||||
impl TextMapPropagator for JaegerPropagator {
|
||||
fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
|
||||
let span = cx.span();
|
||||
let span_context = span.span_context();
|
||||
if !span_context.is_valid() {
|
||||
return;
|
||||
}
|
||||
|
||||
let flags: u8 = if !span_context.is_sampled() {
|
||||
0x00
|
||||
} else if span_context.trace_flags() & TRACE_FLAG_DEBUG == TRACE_FLAG_DEBUG {
|
||||
0x03
|
||||
} else {
|
||||
0x01
|
||||
};
|
||||
injector.set(
|
||||
HEADER_NAME,
|
||||
format!(
|
||||
"{}:{}:0:{flags:x}",
|
||||
span_context.trace_id(),
|
||||
span_context.span_id()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
|
||||
extractor
|
||||
.get(HEADER_NAME)
|
||||
.and_then(parse_header)
|
||||
.map_or_else(
|
||||
|| cx.clone(),
|
||||
|span_context| cx.with_remote_span_context(span_context),
|
||||
)
|
||||
}
|
||||
|
||||
fn fields(&self) -> FieldIter<'_> {
|
||||
FieldIter::new(&self.fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn extract(value: &str) -> Option<SpanContext> {
|
||||
let headers = HashMap::from([(HEADER_NAME.to_owned(), value.to_owned())]);
|
||||
let cx = JaegerPropagator::new().extract(&headers);
|
||||
let span = cx.span();
|
||||
let span_context = span.span_context();
|
||||
span_context.is_valid().then(|| span_context.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_sampled() {
|
||||
let span_context =
|
||||
extract("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:1").unwrap();
|
||||
assert_eq!(
|
||||
span_context.trace_id(),
|
||||
TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
span_context.span_id(),
|
||||
SpanId::from_hex("00f067aa0ba902b7").unwrap()
|
||||
);
|
||||
assert!(span_context.is_sampled());
|
||||
assert!(span_context.is_remote());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_not_sampled() {
|
||||
let span_context =
|
||||
extract("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:0").unwrap();
|
||||
assert!(!span_context.is_sampled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_debug_flag() {
|
||||
let span_context =
|
||||
extract("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:3").unwrap();
|
||||
assert!(span_context.is_sampled());
|
||||
assert_eq!(
|
||||
span_context.trace_flags() & TRACE_FLAG_DEBUG,
|
||||
TRACE_FLAG_DEBUG
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_debug_flag_without_sampled() {
|
||||
let span_context =
|
||||
extract("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:2").unwrap();
|
||||
assert_eq!(span_context.trace_flags(), TraceFlags::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_synapse_header() {
|
||||
// jaeger-client-python URL-encodes the value, drops leading zeros and
|
||||
// sends a non-zero parent span ID
|
||||
let span_context =
|
||||
extract("6f3a0c1d2e4b5a69%3A1b2c3d4e5f6a7b8%3A2a3b4c5d6e7f8091%3A1").unwrap();
|
||||
assert_eq!(
|
||||
span_context.trace_id(),
|
||||
TraceId::from_hex("00000000000000006f3a0c1d2e4b5a69").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
span_context.span_id(),
|
||||
SpanId::from_hex("01b2c3d4e5f6a7b8").unwrap()
|
||||
);
|
||||
assert!(span_context.is_sampled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_short_ids() {
|
||||
let span_context = extract("a3ce929d0e0e4736:f067aa0ba902b7:0:1").unwrap();
|
||||
assert_eq!(
|
||||
span_context.trace_id(),
|
||||
TraceId::from_hex("0000000000000000a3ce929d0e0e4736").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
span_context.span_id(),
|
||||
SpanId::from_hex("00f067aa0ba902b7").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_url_encoded() {
|
||||
let span_context =
|
||||
extract("4bf92f3577b34da6a3ce929d0e0e4736%3A00f067aa0ba902b7%3A0%3A1").unwrap();
|
||||
assert!(span_context.is_sampled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_invalid() {
|
||||
for value in [
|
||||
"",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:1",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:1:0",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:100",
|
||||
":00f067aa0ba902b7:0:1",
|
||||
"04bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:1",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:000f067aa0ba902b7:0:1",
|
||||
"not-hex:00f067aa0ba902b7:0:1",
|
||||
"0:00f067aa0ba902b7:0:1",
|
||||
"4bf92f3577b34da6a3ce929d0e0e4736:0:0:1",
|
||||
] {
|
||||
assert!(extract(value).is_none(), "{value:?} should be rejected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_missing_header() {
|
||||
let headers: HashMap<String, String> = HashMap::new();
|
||||
let cx = JaegerPropagator::new().extract(&headers);
|
||||
assert!(!cx.span().span_context().is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_round_trip() {
|
||||
let span_context = SpanContext::new(
|
||||
TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").unwrap(),
|
||||
SpanId::from_hex("00f067aa0ba902b7").unwrap(),
|
||||
TraceFlags::SAMPLED,
|
||||
false,
|
||||
TraceState::NONE,
|
||||
);
|
||||
let cx = Context::new().with_remote_span_context(span_context.clone());
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
JaegerPropagator::new().inject_context(&cx, &mut headers);
|
||||
assert_eq!(
|
||||
headers.get(HEADER_NAME).map(String::as_str),
|
||||
Some("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:1")
|
||||
);
|
||||
|
||||
let extracted = extract(&headers[HEADER_NAME]).unwrap();
|
||||
assert_eq!(extracted.trace_id(), span_context.trace_id());
|
||||
assert_eq!(extracted.span_id(), span_context.span_id());
|
||||
assert!(extracted.is_sampled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_debug_flag() {
|
||||
let span_context = SpanContext::new(
|
||||
TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").unwrap(),
|
||||
SpanId::from_hex("00f067aa0ba902b7").unwrap(),
|
||||
TraceFlags::SAMPLED | TRACE_FLAG_DEBUG,
|
||||
false,
|
||||
TraceState::NONE,
|
||||
);
|
||||
let cx = Context::new().with_remote_span_context(span_context);
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
JaegerPropagator::new().inject_context(&cx, &mut headers);
|
||||
assert_eq!(
|
||||
headers.get(HEADER_NAME).map(String::as_str),
|
||||
Some("4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:3")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_without_span() {
|
||||
let mut headers = HashMap::new();
|
||||
JaegerPropagator::new().inject_context(&Context::new(), &mut headers);
|
||||
assert!(headers.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user