use doku::Document;
use opentelemetry::propagation::{Extractor, Injector};
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::{
ExporterBuildError, LogExporter, MetricExporter, SpanExporter, WithExportConfig,
};
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::{trace as sdktrace, Resource};
use serde::{Deserialize, Serialize};
use snafu::{ResultExt as _, Snafu};
use tracing::Subscriber;
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
use crate::ServiceInfo;
pub trait TraceContextCarrier {
fn extract_trace_context(&self) -> opentelemetry::Context;
fn inject_trace_context(&mut self);
}
pub trait TraceContextExt: TraceContextCarrier {
fn link_distributed_trace(&self) -> Result<(), Error>;
}
impl<T: TraceContextCarrier> TraceContextExt for T {
fn link_distributed_trace(&self) -> Result<(), Error> {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let parent_cx = self.extract_trace_context();
tracing::Span::current()
.set_parent(parent_cx)
.map_err(|e| Error::LinkDistributedTrace {
source: Box::new(e),
})
}
}
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Could not initialize logging: {source}"))]
InitLog {
source: ExporterBuildError,
},
#[snafu(display("Could not initialize metrics: {source}"))]
InitMetric {
source: ExporterBuildError,
},
#[snafu(display("Could not initialize tracing: {source}"))]
InitTrace {
source: ExporterBuildError,
},
#[snafu(display("Could not link distributed trace: {source}"))]
LinkDistributedTrace {
source: Box<dyn std::error::Error + Send + Sync>,
},
}
#[derive(Debug, Default, Serialize, Deserialize, Document)]
pub struct MetricSettings {
#[doku(example = "http://localhost:4318/v1/metrics")]
pub endpoint: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Document)]
pub struct LogSettings {
#[doku(example = "debug,yourcrate=info")]
pub console_level: String,
#[doku(example = "warn,yourcrate=debug")]
pub otel_level: String,
#[doku(example = "http://localhost:4317")]
pub endpoint: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Document)]
pub struct TraceSettings {
#[doku(example = "http://localhost:4317")]
pub endpoint: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Document)]
pub struct TelemetrySettings {
pub trace: TraceSettings,
pub log: LogSettings,
pub metric: MetricSettings,
}
#[derive(Debug, Default)]
#[must_use = "dropping TelemetryProviders will shut down all telemetry"]
pub struct TelemetryProviders {
meter: Option<SdkMeterProvider>,
tracer: Option<sdktrace::SdkTracerProvider>,
logger: Option<SdkLoggerProvider>,
}
impl Drop for TelemetryProviders {
fn drop(&mut self) {
if let Some(tracer_provider) = self.tracer.take() {
if let Err(err) = tracer_provider.shutdown() {
eprintln!("Error shutting down Telemetry tracer provider: {err}");
}
}
if let Some(logger_provider) = self.logger.take() {
if let Err(err) = logger_provider.shutdown() {
eprintln!("Error shutting down Telemetry logger provider: {err}");
}
}
if let Some(meter_provider) = self.meter.take() {
if let Err(err) = meter_provider.shutdown() {
eprintln!("Error shutting down Telemetry meter provider: {err}");
}
}
}
}
fn init_traces(
service_info: &ServiceInfo,
settings: &TraceSettings,
) -> Result<Option<sdktrace::SdkTracerProvider>, ExporterBuildError> {
match &settings.endpoint {
Some(endpoint) => {
let exporter = SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
Ok(Some(
sdktrace::SdkTracerProvider::builder()
.with_resource(resource)
.with_batch_exporter(exporter)
.build(),
))
}
None => Ok(None),
}
}
fn init_metrics(
service_info: &ServiceInfo,
setting: &MetricSettings,
) -> Result<Option<opentelemetry_sdk::metrics::SdkMeterProvider>, ExporterBuildError> {
match &setting.endpoint {
Some(endpoint) => {
let exporter = MetricExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
let reader = PeriodicReader::builder(exporter).build();
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
Ok(Some(
SdkMeterProvider::builder()
.with_reader(reader)
.with_resource(resource)
.build(),
))
}
None => Ok(None),
}
}
fn init_otel_logs<S>(
service_info: &ServiceInfo,
settings: &LogSettings,
) -> Result<
(
Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
Option<impl tracing_subscriber::layer::Layer<S> + use<S>>,
),
Error,
>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
match &settings.endpoint {
None => Ok((None, None)),
Some(endpoint) => {
let builder = init_otel_logs_builder(service_info, endpoint)?;
let logger_provider = builder.build();
let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
let filter_otel = EnvFilter::new(&settings.otel_level)
.add_directive("hyper=off".parse().unwrap())
.add_directive("opentelemetry=off".parse().unwrap())
.add_directive("tonic=off".parse().unwrap())
.add_directive("h2=off".parse().unwrap())
.add_directive("reqwest=off".parse().unwrap());
let otel_layer = otel_layer.with_filter(filter_otel);
Ok((Some(logger_provider), Some(otel_layer)))
}
}
}
fn init_otel_logs_builder(
service_info: &ServiceInfo,
endpoint: &String,
) -> Result<opentelemetry_sdk::logs::LoggerProviderBuilder, Error> {
let builder = SdkLoggerProvider::builder();
let exporter = LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.with_context(|_| InitLogSnafu {})?;
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
let builder = builder
.with_resource(resource)
.with_batch_exporter(exporter);
Ok(builder)
}
struct LogSubscriberBuilder<'a> {
service_info: &'a ServiceInfo,
settings: &'a LogSettings,
tracer_provider: Option<&'a sdktrace::SdkTracerProvider>,
}
struct BuiltSubscriber<S> {
logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
subscriber: S,
}
impl<'a> LogSubscriberBuilder<'a> {
fn new(service_info: &'a ServiceInfo, settings: &'a LogSettings) -> Self {
Self {
service_info,
settings,
tracer_provider: None,
}
}
fn with_tracer_provider(mut self, provider: &'a sdktrace::SdkTracerProvider) -> Self {
self.tracer_provider = Some(provider);
self
}
fn build(
self,
) -> Result<
BuiltSubscriber<
impl Subscriber + use<'a>,
>,
Error,
> {
let (logger_provider, otel_log_layer) = init_otel_logs(self.service_info, self.settings)?;
let otel_trace_layer = self.tracer_provider.map(|provider| {
let tracer = provider.tracer(self.service_info.name_in_metrics.clone());
let filter = EnvFilter::new(&self.settings.otel_level)
.add_directive("hyper=off".parse().unwrap())
.add_directive("opentelemetry=off".parse().unwrap())
.add_directive("opentelemetry_sdk=off".parse().unwrap())
.add_directive("tonic=off".parse().unwrap())
.add_directive("h2=off".parse().unwrap())
.add_directive("reqwest=off".parse().unwrap());
OpenTelemetryLayer::new(tracer).with_filter(filter)
});
let filter_fmt = EnvFilter::new(&self.settings.console_level);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_thread_names(true)
.with_filter(filter_fmt);
let subscriber = tracing_subscriber::registry()
.with(otel_log_layer)
.with(otel_trace_layer)
.with(fmt_layer);
Ok(BuiltSubscriber {
logger_provider,
subscriber,
})
}
fn init(self) -> Result<Option<opentelemetry_sdk::logs::SdkLoggerProvider>, Error> {
let built = self.build()?;
built.subscriber.init();
Ok(built.logger_provider)
}
}
fn init_logs(
service_info: &ServiceInfo,
settings: &LogSettings,
tracer_provider: Option<&sdktrace::SdkTracerProvider>,
) -> Result<Option<opentelemetry_sdk::logs::SdkLoggerProvider>, Error> {
let mut builder = LogSubscriberBuilder::new(service_info, settings);
if let Some(provider) = tracer_provider {
builder = builder.with_tracer_provider(provider);
}
builder.init()
}
#[must_use]
pub fn init(
service_info: &ServiceInfo,
settings: &TelemetrySettings,
) -> Result<TelemetryProviders, Error> {
init_propagator();
let tracer_provider =
init_traces(service_info, &settings.trace).with_context(|_| InitTraceSnafu {})?;
if let Some(tracer_provider) = &tracer_provider {
global::set_tracer_provider(tracer_provider.clone());
}
let logger_provider = init_logs(service_info, &settings.log, tracer_provider.as_ref())?;
let meter_provider =
init_metrics(service_info, &settings.metric).with_context(|_| InitMetricSnafu {})?;
if let Some(meter_provider) = &meter_provider {
global::set_meter_provider(meter_provider.clone());
}
Ok(TelemetryProviders {
meter: meter_provider,
tracer: tracer_provider,
logger: logger_provider,
})
}
pub struct MetadataExtractor<'a>(pub &'a tonic::metadata::MetadataMap);
impl Extractor for MetadataExtractor<'_> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|v| v.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
["traceparent", "tracestate"]
.into_iter()
.filter(|k| self.0.get(*k).is_some())
.collect()
}
}
pub struct MetadataInjector<'a>(pub &'a mut tonic::metadata::MetadataMap);
impl Injector for MetadataInjector<'_> {
fn set(&mut self, key: &str, value: String) {
if let Ok(key) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes()) {
if let Ok(value) = tonic::metadata::MetadataValue::try_from(&value) {
self.0.insert(key, value);
}
}
}
}
impl TraceContextCarrier for tonic::metadata::MetadataMap {
fn extract_trace_context(&self) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(&MetadataExtractor(self)))
}
fn inject_trace_context(&mut self) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut MetadataInjector(self));
});
}
}
pub fn extract_trace_context(metadata: &tonic::metadata::MetadataMap) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(&MetadataExtractor(metadata)))
}
pub fn link_distributed_trace(metadata: &tonic::metadata::MetadataMap) -> Result<(), Error> {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let parent_cx = extract_trace_context(metadata);
tracing::Span::current()
.set_parent(parent_cx)
.map_err(|e| Error::LinkDistributedTrace {
source: Box::new(e),
})
}
pub fn inject_trace_context(metadata: &mut tonic::metadata::MetadataMap) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut MetadataInjector(metadata));
});
}
pub fn init_propagator() {
global::set_text_map_propagator(TraceContextPropagator::new());
}
pub struct HttpHeaderExtractor<'a>(pub &'a http::HeaderMap);
impl Extractor for HttpHeaderExtractor<'_> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|v| v.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
["traceparent", "tracestate"]
.into_iter()
.filter(|k| self.0.get(*k).is_some())
.collect()
}
}
pub struct HttpHeaderInjector<'a>(pub &'a mut http::HeaderMap);
impl Injector for HttpHeaderInjector<'_> {
fn set(&mut self, key: &str, value: String) {
if let Ok(key) = http::header::HeaderName::from_bytes(key.as_bytes()) {
if let Ok(value) = http::header::HeaderValue::from_str(&value) {
self.0.insert(key, value);
}
}
}
}
impl TraceContextCarrier for http::HeaderMap {
fn extract_trace_context(&self) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(&HttpHeaderExtractor(self)))
}
fn inject_trace_context(&mut self) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut HttpHeaderInjector(self));
});
}
}
pub fn extract_trace_context_http(headers: &http::HeaderMap) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(&HttpHeaderExtractor(headers)))
}
pub fn link_distributed_trace_http(headers: &http::HeaderMap) -> Result<(), Error> {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let parent_cx = extract_trace_context_http(headers);
tracing::Span::current()
.set_parent(parent_cx)
.map_err(|e| Error::LinkDistributedTrace {
source: Box::new(e),
})
}
pub fn inject_trace_context_http(headers: &mut http::HeaderMap) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut HttpHeaderInjector(headers));
});
}
#[derive(Clone)]
pub struct GrpcTraceContextLayer {
service_name: &'static str,
}
impl GrpcTraceContextLayer {
pub fn new(service_name: &'static str) -> Self {
Self { service_name }
}
}
impl<S> tower::Layer<S> for GrpcTraceContextLayer {
type Service = GrpcTraceContextService<S>;
fn layer(&self, inner: S) -> Self::Service {
GrpcTraceContextService {
inner,
service_name: self.service_name,
}
}
}
#[derive(Clone)]
pub struct GrpcTraceContextService<S> {
inner: S,
service_name: &'static str,
}
impl<S, B> tower::Service<http::Request<B>> for GrpcTraceContextService<S>
where
S: tower::Service<http::Request<B>> + Clone + Send + 'static,
S::Future: Send,
B: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: http::Request<B>) -> Self::Future {
use tracing::Instrument;
use tracing_opentelemetry::OpenTelemetrySpanExt;
let parent_cx = extract_trace_context_http(request.headers());
let span = tracing::info_span!("grpc_request", service = self.service_name);
let _ = span.set_parent(parent_cx);
let mut inner = self.inner.clone();
Box::pin(async move { inner.call(request).await }.instrument(span))
}
}
impl TraceContextCarrier for std::collections::HashMap<String, String> {
fn extract_trace_context(&self) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(self))
}
fn inject_trace_context(&mut self) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, self);
});
}
}
pub fn inject_trace_context_map(headers: &mut std::collections::HashMap<String, String>) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let cx = tracing::Span::current().context();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, headers);
});
}
pub fn extract_trace_context_map(
headers: &std::collections::HashMap<String, String>,
) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(headers))
}
pub fn link_distributed_trace_map(
headers: &std::collections::HashMap<String, String>,
) -> Result<(), Error> {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let parent_cx = extract_trace_context_map(headers);
tracing::Span::current()
.set_parent(parent_cx)
.map_err(|e| Error::LinkDistributedTrace {
source: Box::new(e),
})
}
pub fn set_span_parent(span: &tracing::Span, parent_cx: opentelemetry::Context) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _ = span.set_parent(parent_cx);
}
pub mod prelude {
pub use super::{
init, GrpcTraceContextLayer, TelemetryProviders, TelemetrySettings, TraceContextCarrier,
TraceContextExt,
};
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::trace::{
SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState, Tracer,
};
use std::collections::HashMap;
fn init_test_propagator() {
use opentelemetry::propagation::TextMapCompositePropagator;
use opentelemetry_sdk::propagation::TraceContextPropagator;
let propagator =
TextMapCompositePropagator::new(vec![Box::new(TraceContextPropagator::new())]);
global::set_text_map_propagator(propagator);
}
#[test]
fn test_inject_and_extract_trace_context_roundtrip() {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _provider = init_tracing_with_otel();
let trace_id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap();
let span_id = SpanId::from_hex("b7ad6b7169203331").unwrap();
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let parent_cx = opentelemetry::Context::new().with_remote_span_context(span_context);
let span = tracing::info_span!("test_roundtrip_span");
let _ = span.set_parent(parent_cx);
let _enter = span.enter();
let mut headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut headers);
assert!(
headers.contains_key("traceparent"),
"traceparent header should be present"
);
let traceparent = headers.get("traceparent").unwrap();
assert!(
traceparent.starts_with("00-0af7651916cd43dd8448eb211c80319c-"),
"traceparent should contain the correct trace ID"
);
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let extracted_span_context = extracted_span.span_context();
assert_eq!(
extracted_span_context.trace_id(),
trace_id,
"trace ID should match"
);
assert!(
extracted_span_context.trace_flags().is_sampled(),
"sampled flag should be preserved"
);
}
#[test]
fn test_extract_empty_headers_returns_empty_context() {
init_test_propagator();
let headers: HashMap<String, String> = HashMap::new();
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let span_context = extracted_span.span_context();
assert!(
!span_context.is_valid(),
"extracted context from empty headers should be invalid"
);
}
#[test]
fn test_extract_invalid_traceparent_returns_empty_context() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"invalid-traceparent-value".to_string(),
);
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let span_context = extracted_span.span_context();
assert!(
!span_context.is_valid(),
"extracted context from invalid traceparent should be invalid"
);
}
#[test]
fn test_inject_without_active_span_produces_no_traceparent() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut headers);
if let Some(traceparent) = headers.get("traceparent") {
assert!(
traceparent.contains("00000000000000000000000000000000") || traceparent.is_empty(),
"traceparent without active span should be empty or have zero trace ID"
);
}
}
#[test]
fn test_trace_context_preserves_tracestate() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
);
headers.insert("tracestate".to_string(), "congo=t61rcWkgMzE".to_string());
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let span_context = extracted_span.span_context();
assert!(span_context.is_valid(), "span context should be valid");
assert!(
!span_context.trace_state().header().is_empty(),
"tracestate should be preserved"
);
}
#[test]
fn test_inject_extract_with_real_span() {
init_test_propagator();
let tracer = global::tracer("test-tracer");
let span = tracer.start("test-span");
let cx = opentelemetry::Context::current_with_span(span);
let original_span = cx.span();
let original_trace_id = original_span.span_context().trace_id();
let _guard = cx.attach();
let mut headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut headers);
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let extracted_span_context = extracted_span.span_context();
assert_eq!(
extracted_span_context.trace_id(),
original_trace_id,
"trace ID should be preserved through inject/extract cycle"
);
}
fn init_tracing_with_otel() -> opentelemetry_sdk::trace::SdkTracerProvider {
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
init_test_propagator();
let provider = SdkTracerProvider::builder().build();
let tracer = provider.tracer("test-tracer");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = tracing_subscriber::registry().with(otel_layer);
let _ = subscriber.try_init();
provider
}
fn with_otel_subscriber<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::layer::SubscriberExt;
init_test_propagator();
let provider = SdkTracerProvider::builder().build();
let tracer = provider.tracer("test-tracer");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = tracing_subscriber::registry().with(otel_layer);
tracing::subscriber::with_default(subscriber, f)
}
fn assert_valid_traceparent(traceparent: &str) {
assert!(
traceparent.starts_with("00-"),
"traceparent should start with version 00"
);
assert!(
!traceparent.contains("00000000000000000000000000000000"),
"traceparent should have a valid (non-zero) trace ID"
);
}
#[test]
fn test_inject_trace_context_from_tracing_span() {
let _provider = init_tracing_with_otel();
let span = tracing::info_span!("test_span_for_injection");
let _enter = span.enter();
let mut map_headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut map_headers);
assert!(map_headers.contains_key("traceparent"));
assert_valid_traceparent(map_headers.get("traceparent").unwrap());
let mut grpc_metadata = tonic::metadata::MetadataMap::new();
inject_trace_context(&mut grpc_metadata);
let grpc_traceparent = grpc_metadata.get("traceparent").unwrap().to_str().unwrap();
assert_valid_traceparent(grpc_traceparent);
let mut http_headers = http::HeaderMap::new();
inject_trace_context_http(&mut http_headers);
let http_traceparent = http_headers.get("traceparent").unwrap().to_str().unwrap();
assert_valid_traceparent(http_traceparent);
}
#[test]
fn test_nested_tracing_spans_propagate_trace_id() {
let _provider = init_tracing_with_otel();
let parent_span = tracing::info_span!("parent_span");
let _parent_enter = parent_span.enter();
let mut parent_headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut parent_headers);
let parent_traceparent = parent_headers.get("traceparent").unwrap().clone();
let parent_trace_id: String = parent_traceparent.split('-').nth(1).unwrap().to_string();
let child_span = tracing::info_span!("child_span");
let _child_enter = child_span.enter();
let mut child_headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut child_headers);
let child_traceparent = child_headers.get("traceparent").unwrap();
let child_trace_id: String = child_traceparent.split('-').nth(1).unwrap().to_string();
assert_eq!(
parent_trace_id, child_trace_id,
"nested spans should share the same trace ID"
);
let parent_span_id: String = parent_traceparent.split('-').nth(2).unwrap().to_string();
let child_span_id: String = child_traceparent.split('-').nth(2).unwrap().to_string();
assert_ne!(
parent_span_id, child_span_id,
"nested spans should have different span IDs"
);
}
#[test]
fn test_inject_trace_context_roundtrip_with_tracing_span() {
let _provider = init_tracing_with_otel();
let span = tracing::info_span!("roundtrip_test_span");
let _enter = span.enter();
let mut headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut headers);
let extracted_cx = extract_trace_context_map(&headers);
let extracted_span = extracted_cx.span();
let extracted_span_context = extracted_span.span_context();
assert!(
extracted_span_context.is_valid(),
"extracted span context should be valid"
);
let traceparent = headers.get("traceparent").unwrap();
let original_trace_id: String = traceparent.split('-').nth(1).unwrap().to_string();
let extracted_trace_id = format!("{:032x}", extracted_span_context.trace_id());
assert_eq!(
original_trace_id, extracted_trace_id,
"trace ID should survive inject/extract roundtrip"
);
}
#[test]
fn test_metadata_extractor_get_returns_value() {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("traceparent", "00-abc123-def456-01".parse().unwrap());
let extractor = MetadataExtractor(&metadata);
let value = extractor.get("traceparent");
assert!(value.is_some(), "get should return Some for existing key");
assert_eq!(value.unwrap(), "00-abc123-def456-01");
}
#[test]
fn test_metadata_extractor_get_returns_none_for_missing() {
let metadata = tonic::metadata::MetadataMap::new();
let extractor = MetadataExtractor(&metadata);
let value = extractor.get("nonexistent");
assert!(value.is_none(), "get should return None for missing key");
}
#[test]
fn test_metadata_extractor_keys_returns_trace_context_keys() {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("traceparent", "value1".parse().unwrap());
metadata.insert("tracestate", "value2".parse().unwrap());
metadata.insert("custom-header", "value3".parse().unwrap());
let extractor = MetadataExtractor(&metadata);
let keys = extractor.keys();
assert_eq!(keys.len(), 2, "keys should only contain trace context keys");
assert!(
keys.contains(&"traceparent"),
"keys should contain traceparent"
);
assert!(
keys.contains(&"tracestate"),
"keys should contain tracestate"
);
}
#[test]
fn test_metadata_extractor_keys_returns_only_present_keys() {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("traceparent", "value1".parse().unwrap());
let extractor = MetadataExtractor(&metadata);
let keys = extractor.keys();
assert_eq!(
keys.len(),
1,
"keys should only contain present trace context keys"
);
assert!(
keys.contains(&"traceparent"),
"keys should contain traceparent"
);
}
#[test]
fn test_http_header_extractor_get_returns_value() {
let mut headers = http::HeaderMap::new();
headers.insert("traceparent", "00-abc123-def456-01".parse().unwrap());
let extractor = HttpHeaderExtractor(&headers);
let value = extractor.get("traceparent");
assert!(value.is_some(), "get should return Some for existing key");
assert_eq!(value.unwrap(), "00-abc123-def456-01");
}
#[test]
fn test_http_header_extractor_get_returns_none_for_missing() {
let headers = http::HeaderMap::new();
let extractor = HttpHeaderExtractor(&headers);
let value = extractor.get("nonexistent");
assert!(value.is_none(), "get should return None for missing key");
}
#[test]
fn test_http_header_extractor_keys_returns_trace_context_keys() {
let mut headers = http::HeaderMap::new();
headers.insert("traceparent", "value1".parse().unwrap());
headers.insert("tracestate", "value2".parse().unwrap());
headers.insert("x-custom-header", "value3".parse().unwrap());
let extractor = HttpHeaderExtractor(&headers);
let keys = extractor.keys();
assert_eq!(keys.len(), 2, "keys should only contain trace context keys");
assert!(
keys.contains(&"traceparent"),
"keys should contain traceparent"
);
assert!(
keys.contains(&"tracestate"),
"keys should contain tracestate"
);
}
#[test]
fn test_http_header_extractor_keys_returns_only_present_keys() {
let mut headers = http::HeaderMap::new();
headers.insert("traceparent", "value1".parse().unwrap());
let extractor = HttpHeaderExtractor(&headers);
let keys = extractor.keys();
assert_eq!(
keys.len(),
1,
"keys should only contain present trace context keys"
);
assert!(
keys.contains(&"traceparent"),
"keys should contain traceparent"
);
}
fn assert_extracted_trace_id(context: &opentelemetry::Context, expected_trace_id: &str) {
let span = context.span();
let span_context = span.span_context();
assert!(span_context.is_valid(), "span context should be valid");
assert_eq!(
format!("{:032x}", span_context.trace_id()),
expected_trace_id,
"trace ID should match"
);
}
#[test]
fn test_extract_trace_context_grpc_with_valid_headers() {
init_test_propagator();
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"traceparent",
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
.parse()
.unwrap(),
);
let context = extract_trace_context(&metadata);
assert_extracted_trace_id(&context, "0af7651916cd43dd8448eb211c80319c");
}
#[test]
fn test_extract_trace_context_http_with_valid_headers() {
init_test_propagator();
let mut headers = http::HeaderMap::new();
headers.insert(
"traceparent",
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
.parse()
.unwrap(),
);
let context = extract_trace_context_http(&headers);
assert_extracted_trace_id(&context, "0af7651916cd43dd8448eb211c80319c");
}
#[test]
fn test_extract_trace_context_map_with_valid_headers() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
);
let context = extract_trace_context_map(&headers);
assert_extracted_trace_id(&context, "0af7651916cd43dd8448eb211c80319c");
}
#[test]
fn test_set_span_parent_actually_links() {
with_otel_subscriber(|| {
let trace_id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap();
let span_id = SpanId::from_hex("b7ad6b7169203331").unwrap();
let remote_span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
true,
TraceState::default(),
);
let parent_cx =
opentelemetry::Context::new().with_remote_span_context(remote_span_context);
let span = tracing::info_span!("test_set_parent");
set_span_parent(&span, parent_cx);
let _enter = span.enter();
let mut headers: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut headers);
let traceparent = headers.get("traceparent").unwrap();
assert!(
traceparent.contains("0af7651916cd43dd8448eb211c80319c"),
"trace ID should be preserved after set_span_parent"
);
});
}
#[test]
fn test_link_distributed_trace_grpc_actually_links() {
with_otel_subscriber(|| {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"traceparent",
"00-11111111111111111111111111111111-aaaaaaaaaaaaaaaa-01"
.parse()
.unwrap(),
);
let span = tracing::info_span!("test_link_grpc");
let parent_cx = metadata.extract_trace_context();
set_span_parent(&span, parent_cx);
let _enter = span.enter();
let mut verify: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut verify);
let traceparent = verify.get("traceparent").unwrap();
assert!(
traceparent.contains("11111111111111111111111111111111"),
"link_distributed_trace should link the span to trace 11111111111111111111111111111111, got {traceparent}"
);
});
}
#[test]
fn test_link_distributed_trace_http_actually_links() {
with_otel_subscriber(|| {
let mut headers = http::HeaderMap::new();
headers.insert(
"traceparent",
"00-22222222222222222222222222222222-bbbbbbbbbbbbbbbb-01"
.parse()
.unwrap(),
);
let span = tracing::info_span!("test_link_http");
let parent_cx = headers.extract_trace_context();
set_span_parent(&span, parent_cx);
let _enter = span.enter();
let mut verify: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut verify);
let traceparent = verify.get("traceparent").unwrap();
assert!(
traceparent.contains("22222222222222222222222222222222"),
"link_distributed_trace_http should link the span"
);
});
}
#[test]
fn test_link_distributed_trace_map_actually_links() {
with_otel_subscriber(|| {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"00-33333333333333333333333333333333-cccccccccccccccc-01".to_string(),
);
let span = tracing::info_span!("test_link_map");
let parent_cx = headers.extract_trace_context();
set_span_parent(&span, parent_cx);
let _enter = span.enter();
let mut verify: HashMap<String, String> = HashMap::new();
inject_trace_context_map(&mut verify);
let traceparent = verify.get("traceparent").unwrap();
assert!(
traceparent.contains("33333333333333333333333333333333"),
"link_distributed_trace_map should link the span"
);
});
}
#[test]
fn test_init_propagator_enables_trace_context_propagation() {
init_propagator();
let trace_id = TraceId::from_hex("1234567890abcdef1234567890abcdef").unwrap();
let span_id = SpanId::from_hex("fedcba0987654321").unwrap();
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let cx = opentelemetry::Context::new().with_remote_span_context(span_context);
let mut headers: HashMap<String, String> = HashMap::new();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut headers);
});
assert!(
headers.contains_key("traceparent"),
"init_propagator should enable traceparent injection"
);
let traceparent = headers.get("traceparent").unwrap();
assert!(
traceparent.contains("1234567890abcdef1234567890abcdef"),
"traceparent should contain the trace ID"
);
}
#[test]
fn test_metadata_map_extract_trace_context_returns_valid_context() {
init_test_propagator();
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"traceparent",
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
.parse()
.unwrap(),
);
let context = TraceContextCarrier::extract_trace_context(&metadata);
let span = context.span();
let span_context = span.span_context();
assert!(span_context.is_valid(), "span context should be valid");
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"0af7651916cd43dd8448eb211c80319c",
"trace ID should be extracted from headers, not default"
);
}
#[test]
fn test_http_header_map_extract_trace_context_returns_valid_context() {
init_test_propagator();
let mut headers = http::HeaderMap::new();
headers.insert(
"traceparent",
"00-1234567890abcdef1234567890abcdef-b7ad6b7169203331-01"
.parse()
.unwrap(),
);
let context = TraceContextCarrier::extract_trace_context(&headers);
let span = context.span();
let span_context = span.span_context();
assert!(span_context.is_valid(), "span context should be valid");
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"1234567890abcdef1234567890abcdef",
"trace ID should be extracted from headers, not default"
);
}
#[test]
fn test_hashmap_extract_trace_context_returns_valid_context() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"00-abcdef1234567890abcdef1234567890-b7ad6b7169203331-01".to_string(),
);
let context = TraceContextCarrier::extract_trace_context(&headers);
let span = context.span();
let span_context = span.span_context();
assert!(span_context.is_valid(), "span context should be valid");
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"abcdef1234567890abcdef1234567890",
"trace ID should be extracted from headers, not default"
);
}
#[test]
fn test_metadata_map_inject_trace_context_modifies_carrier() {
let _provider = init_tracing_with_otel();
let trace_id = TraceId::from_hex("fedcba9876543210fedcba9876543210").unwrap();
let span_id = SpanId::from_hex("1234567890abcdef").unwrap();
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let parent_cx = opentelemetry::Context::new().with_remote_span_context(span_context);
let span = tracing::info_span!("test_grpc_inject");
{
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _ = span.set_parent(parent_cx);
}
let _enter = span.enter();
let mut metadata = tonic::metadata::MetadataMap::new();
assert!(
metadata.get("traceparent").is_none(),
"metadata should start empty"
);
TraceContextCarrier::inject_trace_context(&mut metadata);
assert!(
metadata.get("traceparent").is_some(),
"inject_trace_context should add traceparent"
);
let traceparent = metadata.get("traceparent").unwrap().to_str().unwrap();
assert!(
traceparent.contains("fedcba9876543210fedcba9876543210"),
"injected traceparent should contain the trace ID"
);
}
#[test]
fn test_http_header_map_inject_trace_context_modifies_carrier() {
let _provider = init_tracing_with_otel();
let trace_id = TraceId::from_hex("11111111111111111111111111111111").unwrap();
let span_id = SpanId::from_hex("2222222222222222").unwrap();
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let parent_cx = opentelemetry::Context::new().with_remote_span_context(span_context);
let span = tracing::info_span!("test_http_inject");
{
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _ = span.set_parent(parent_cx);
}
let _enter = span.enter();
let mut headers = http::HeaderMap::new();
assert!(
headers.get("traceparent").is_none(),
"headers should start empty"
);
TraceContextCarrier::inject_trace_context(&mut headers);
assert!(
headers.get("traceparent").is_some(),
"inject_trace_context should add traceparent"
);
let traceparent = headers.get("traceparent").unwrap().to_str().unwrap();
assert!(
traceparent.contains("11111111111111111111111111111111"),
"injected traceparent should contain the trace ID"
);
}
#[test]
fn test_hashmap_inject_trace_context_modifies_carrier() {
let _provider = init_tracing_with_otel();
let trace_id = TraceId::from_hex("33333333333333333333333333333333").unwrap();
let span_id = SpanId::from_hex("4444444444444444").unwrap();
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let parent_cx = opentelemetry::Context::new().with_remote_span_context(span_context);
let span = tracing::info_span!("test_map_inject");
{
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _ = span.set_parent(parent_cx);
}
let _enter = span.enter();
let mut headers: HashMap<String, String> = HashMap::new();
assert!(
headers.get("traceparent").is_none(),
"headers should start empty"
);
TraceContextCarrier::inject_trace_context(&mut headers);
assert!(
headers.get("traceparent").is_some(),
"inject_trace_context should add traceparent"
);
let traceparent = headers.get("traceparent").unwrap();
assert!(
traceparent.contains("33333333333333333333333333333333"),
"injected traceparent should contain the trace ID"
);
}
#[test]
fn test_link_distributed_trace_grpc_calls_set_parent() {
init_test_propagator();
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"traceparent",
"00-aaaabbbbccccddddaaaabbbbccccdddd-1111222233334444-01"
.parse()
.unwrap(),
);
let _ = link_distributed_trace(&metadata);
let ctx = extract_trace_context(&metadata);
let span_ref = ctx.span();
let span_context = span_ref.span_context();
assert!(span_context.is_valid());
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"aaaabbbbccccddddaaaabbbbccccdddd"
);
}
#[test]
fn test_link_distributed_trace_http_extracts_and_links() {
init_test_propagator();
let mut headers = http::HeaderMap::new();
headers.insert(
"traceparent",
"00-55556666777788885555666677778888-9999aaaabbbbcccc-01"
.parse()
.unwrap(),
);
let _ = link_distributed_trace_http(&headers);
let ctx = extract_trace_context_http(&headers);
let span = ctx.span();
let span_context = span.span_context();
assert!(span_context.is_valid());
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"55556666777788885555666677778888"
);
}
#[test]
fn test_link_distributed_trace_map_extracts_and_links() {
init_test_propagator();
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"traceparent".to_string(),
"00-ddddeeeeffff0000ddddeeeeffff0000-1234567890abcdef-01".to_string(),
);
let _ = link_distributed_trace_map(&headers);
let ctx = extract_trace_context_map(&headers);
let span = ctx.span();
let span_context = span.span_context();
assert!(span_context.is_valid());
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"ddddeeeeffff0000ddddeeeeffff0000"
);
}
#[test]
fn test_trace_context_ext_link_distributed_trace_extracts_context() {
init_test_propagator();
let mut headers = http::HeaderMap::new();
headers.insert(
"traceparent",
"00-11112222333344441111222233334444-5555666677778888-01"
.parse()
.unwrap(),
);
let _ = super::TraceContextExt::link_distributed_trace(&headers);
let ctx = TraceContextCarrier::extract_trace_context(&headers);
let span = ctx.span();
let span_context = span.span_context();
assert!(span_context.is_valid());
assert_eq!(
format!("{:032x}", span_context.trace_id()),
"11112222333344441111222233334444"
);
}
#[test]
fn test_metadata_injector_set_adds_header() {
let mut metadata = tonic::metadata::MetadataMap::new();
{
let mut injector = MetadataInjector(&mut metadata);
injector.set("traceparent", "00-test-value-01".to_string());
}
assert!(
metadata.get("traceparent").is_some(),
"set should add the header"
);
assert_eq!(
metadata.get("traceparent").unwrap().to_str().unwrap(),
"00-test-value-01"
);
}
#[test]
fn test_metadata_injector_set_handles_invalid_key_gracefully() {
let mut metadata = tonic::metadata::MetadataMap::new();
{
let mut injector = MetadataInjector(&mut metadata);
injector.set("invalid key with spaces", "value".to_string());
}
assert!(
metadata.is_empty(),
"invalid header keys should be handled gracefully"
);
}
#[test]
fn test_http_header_injector_set_adds_header() {
let mut headers = http::HeaderMap::new();
{
let mut injector = HttpHeaderInjector(&mut headers);
injector.set("traceparent", "00-http-test-01".to_string());
}
assert!(
headers.get("traceparent").is_some(),
"set should add the header"
);
assert_eq!(
headers.get("traceparent").unwrap().to_str().unwrap(),
"00-http-test-01"
);
}
#[test]
fn test_http_header_injector_set_handles_invalid_key_gracefully() {
let mut headers = http::HeaderMap::new();
{
let mut injector = HttpHeaderInjector(&mut headers);
injector.set("invalid key", "value".to_string());
}
assert!(
headers.is_empty(),
"invalid header keys should be handled gracefully"
);
}
#[test]
fn test_init_otel_logs_builder_returns_configured_builder() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let endpoint = "http://localhost:4317".to_string();
let result = super::init_otel_logs_builder(&service_info, &endpoint);
assert!(
result.is_ok(),
"init_otel_logs_builder should return Ok with valid endpoint"
);
let builder = result.unwrap();
let provider = builder.build();
let shutdown_result = provider.shutdown();
assert!(
shutdown_result.is_ok(),
"provider built from configured builder should shutdown cleanly"
);
});
}
#[test]
fn test_init_traces_with_endpoint_returns_provider() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = TraceSettings {
endpoint: Some("http://localhost:4317".to_string()),
};
let result = super::init_traces(&service_info, &settings);
assert!(result.is_ok(), "init_traces should succeed");
let provider = result.unwrap();
assert!(
provider.is_some(),
"init_traces should return Some(provider) when endpoint is configured"
);
if let Some(p) = provider {
let _ = p.shutdown();
}
});
}
#[test]
fn test_init_traces_without_endpoint_returns_none() {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = TraceSettings { endpoint: None };
let result = super::init_traces(&service_info, &settings);
assert!(result.is_ok(), "init_traces should succeed");
let provider = result.unwrap();
assert!(
provider.is_none(),
"init_traces should return None when endpoint is not configured"
);
}
#[test]
fn test_init_metrics_with_endpoint_returns_provider() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = MetricSettings {
endpoint: Some("http://localhost:4317".to_string()),
};
let result = super::init_metrics(&service_info, &settings);
assert!(result.is_ok(), "init_metrics should succeed");
let provider = result.unwrap();
assert!(
provider.is_some(),
"init_metrics should return Some(provider) when endpoint is configured"
);
if let Some(p) = provider {
let _ = p.shutdown();
}
});
}
#[test]
fn test_init_metrics_without_endpoint_returns_none() {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = MetricSettings { endpoint: None };
let result = super::init_metrics(&service_info, &settings);
assert!(result.is_ok(), "init_metrics should succeed");
let provider = result.unwrap();
assert!(
provider.is_none(),
"init_metrics should return None when endpoint is not configured"
);
}
#[test]
fn test_log_subscriber_builder_new_sets_fields() {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = LogSettings {
console_level: "info".to_string(),
otel_level: "info".to_string(),
endpoint: None,
};
let builder = super::LogSubscriberBuilder::new(&service_info, &settings);
assert_eq!(builder.service_info.name, "test-service");
assert_eq!(builder.settings.console_level, "info");
assert!(builder.tracer_provider.is_none());
}
#[test]
fn test_log_subscriber_builder_with_tracer_provider_sets_provider() {
use opentelemetry_sdk::trace::SdkTracerProvider;
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = LogSettings {
console_level: "info".to_string(),
otel_level: "info".to_string(),
endpoint: None,
};
let tracer_provider = SdkTracerProvider::builder().build();
let builder = super::LogSubscriberBuilder::new(&service_info, &settings)
.with_tracer_provider(&tracer_provider);
assert!(builder.tracer_provider.is_some());
let _ = tracer_provider.shutdown();
}
#[test]
fn test_log_subscriber_builder_build_returns_working_subscriber() {
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = LogSettings {
console_level: "info".to_string(),
otel_level: "info".to_string(),
endpoint: None, };
let result = super::LogSubscriberBuilder::new(&service_info, &settings).build();
assert!(result.is_ok(), "build() should succeed");
let built = result.unwrap();
assert!(
built.logger_provider.is_none(),
"logger_provider should be None without OTel endpoint"
);
use std::sync::atomic::{AtomicBool, Ordering};
static LOG_RECEIVED: AtomicBool = AtomicBool::new(false);
struct TestLayer;
impl<S: Subscriber> tracing_subscriber::Layer<S> for TestLayer {
fn on_event(
&self,
_event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
LOG_RECEIVED.store(true, Ordering::SeqCst);
}
}
use tracing_subscriber::layer::SubscriberExt;
let subscriber_with_test = built.subscriber.with(TestLayer);
tracing::subscriber::with_default(subscriber_with_test, || {
tracing::info!("test log message");
});
assert!(
LOG_RECEIVED.load(Ordering::SeqCst),
"subscriber from build() should process log events"
);
}
#[test]
fn test_log_subscriber_builder_build_with_tracer_provider() {
use opentelemetry_sdk::trace::SdkTracerProvider;
let service_info = crate::ServiceInfo {
name: "test-service",
name_in_metrics: "test_service".to_string(),
version: "1.0.0",
author: "Test",
description: "Test service",
};
let settings = LogSettings {
console_level: "info".to_string(),
otel_level: "info".to_string(),
endpoint: None,
};
let tracer_provider = SdkTracerProvider::builder().build();
let result = super::LogSubscriberBuilder::new(&service_info, &settings)
.with_tracer_provider(&tracer_provider)
.build();
assert!(
result.is_ok(),
"build() should succeed with tracer_provider"
);
let built = result.unwrap();
tracing::subscriber::with_default(built.subscriber, || {
let span = tracing::info_span!("test_span_with_otel");
let _enter = span.enter();
tracing::info!("inside span");
});
let _ = tracer_provider.shutdown();
}
}