Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ping.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use serde::Deserialize;
use tracing_batteries::prelude::*;

use crate::telemetry::TracePropagationExt;

/// Configuration for an HTTP-based cron monitoring solution (such as
/// [Sentry Cron Monitors](https://docs.sentry.io/product/crons/) or
/// [healthchecks.io](https://healthchecks.io)).
Expand Down Expand Up @@ -71,6 +73,7 @@ impl Pinger {
.client
.get(url)
.header("User-Agent", "SierraSoftworks/github-backup")
.with_trace_context()
.send()
.await
{
Expand Down
2 changes: 2 additions & 0 deletions src/telemetry/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod trace_propagation;
mod traced_stream;

pub use trace_propagation::*;
pub use traced_stream::*;
84 changes: 84 additions & 0 deletions src/telemetry/trace_propagation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use tracing_batteries::prelude::*;

/// An [`OpenTelemetryPropagationInjector`] which writes the fields emitted by
/// the global text map propagator (such as the W3C `traceparent` header) into a
/// [`reqwest`] header map, silently skipping anything that is not a valid HTTP
/// header.
struct HeaderInjector<'a>(&'a mut reqwest::header::HeaderMap);

impl OpenTelemetryPropagationInjector for HeaderInjector<'_> {
fn set(&mut self, key: &str, value: String) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(key.as_bytes())
&& let Ok(value) = reqwest::header::HeaderValue::from_str(&value)
{
self.0.insert(name, value);
}
}
}

/// Builds the set of trace propagation headers for the provided OpenTelemetry
/// [`context`](opentelemetry::Context) using the globally configured text map
/// propagator.
///
/// When the context does not carry a valid span (for example because telemetry
/// export is disabled) no headers are produced, leaving the request untouched.
fn trace_context_headers(context: &opentelemetry::Context) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
get_text_map_propagator(|propagator| {
propagator.inject_context(context, &mut HeaderInjector(&mut headers));
});
headers
}

/// Extension trait for [`reqwest::RequestBuilder`] which attaches the current
/// span's trace context to an outgoing request, allowing downstream services to
/// correlate their telemetry with the backup run that issued the request.
pub trait TracePropagationExt {
/// Injects the current span's trace context (for example the W3C
/// `traceparent` header) into the request so that it can be tied back to the
/// originating trace when investigating cross-service failures.
fn with_trace_context(self) -> Self;
}

impl TracePropagationExt for reqwest::RequestBuilder {
fn with_trace_context(self) -> Self {
let headers = trace_context_headers(&Span::current().context());
self.headers(headers)
}
}

#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState};

#[test]
fn injects_traceparent_for_valid_context() {
set_text_map_propagator(TraceContextPropagator::new());

let span_context = SpanContext::new(
TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap(),
SpanId::from_hex("b7ad6b7169203331").unwrap(),
TraceFlags::SAMPLED,
true,
TraceState::default(),
);
let context = opentelemetry::Context::new().with_remote_span_context(span_context);

let headers = trace_context_headers(&context);

assert_eq!(
headers.get("traceparent").and_then(|v| v.to_str().ok()),
Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
);
}

#[test]
fn no_headers_for_empty_context() {
set_text_map_propagator(TraceContextPropagator::new());

let headers = trace_context_headers(&opentelemetry::Context::new());

assert!(headers.get("traceparent").is_none());
}
}