pub use super::span_names::CONSUMER_PROCESS;
pub use super::span_names::PRODUCER_SEND;
#[cfg(feature = "otel-traces")]
use std::collections::HashSet;
use rocketmq_model::message::MessageQueue;
use crate::propagation::MessagePropertiesLike;
use crate::TelemetryHandle;
#[derive(Clone, Copy)]
pub struct MessageSpanContext<'a> {
properties: &'a dyn MessagePropertiesLike,
body_size: Option<usize>,
}
impl<'a> MessageSpanContext<'a> {
pub fn new(properties: &'a dyn MessagePropertiesLike, body_size: Option<usize>) -> Self {
Self { properties, body_size }
}
}
pub fn producer_send_span(telemetry: &TelemetryHandle) -> tracing::Span {
#[cfg(feature = "otel-traces")]
{
if !telemetry.trace_policy().enabled {
return tracing::Span::none();
}
tracing::info_span!(
PRODUCER_SEND,
messaging.system = "rocketmq",
messaging.message.id = tracing::field::Empty,
messaging.message.body.size = tracing::field::Empty,
messaging.rocketmq.message.keys = tracing::field::Empty,
)
}
#[cfg(not(feature = "otel-traces"))]
{
let _ = telemetry;
tracing::Span::none()
}
}
#[cfg(feature = "otel-traces")]
struct ConsumerTraceContextPlan<'a> {
first_message: Option<MessageSpanContext<'a>>,
parent_message: Option<MessageSpanContext<'a>>,
links: Vec<opentelemetry::trace::SpanContext>,
}
#[cfg(feature = "otel-traces")]
fn consumer_trace_context_plan<'a>(
telemetry: &TelemetryHandle,
messages: impl IntoIterator<Item = MessageSpanContext<'a>>,
) -> ConsumerTraceContextPlan<'a> {
use opentelemetry::trace::TraceContextExt;
let mut first_message = None;
let mut parent_message = None;
let mut seen_remote_parents = HashSet::new();
let mut links = Vec::new();
for message in messages {
if first_message.is_none() {
first_message = Some(message);
}
let remote_context = crate::propagation::extract_context_with_handle(telemetry, message.properties);
let span_context = remote_context.span().span_context().clone();
if !span_context.is_valid() || !span_context.is_remote() {
continue;
}
if parent_message.is_none() {
parent_message = Some(message);
}
if seen_remote_parents.insert((span_context.trace_id(), span_context.span_id())) {
links.push(span_context);
}
}
ConsumerTraceContextPlan {
first_message,
parent_message,
links,
}
}
pub fn consumer_process_span<'a>(
telemetry: &TelemetryHandle,
messages: impl IntoIterator<Item = MessageSpanContext<'a>>,
message_count: usize,
consumer_group: &str,
message_queue: &MessageQueue,
consume_mode: &'static str,
) -> tracing::Span {
#[cfg(feature = "otel-traces")]
{
if !telemetry.trace_policy().enabled {
return tracing::Span::none();
}
let context_plan = consumer_trace_context_plan(telemetry, messages);
let span = tracing::info_span!(
CONSUMER_PROCESS,
messaging_system = "rocketmq",
messaging_operation_name = "process",
messaging_destination_name = %message_queue.topic(),
rocketmq_consumer_group = consumer_group,
rocketmq_broker_name = %message_queue.broker_name(),
rocketmq_queue_id = message_queue.queue_id(),
rocketmq_consume_mode = consume_mode,
messaging_batch_message_count = message_count as i64,
messaging.message.id = tracing::field::Empty,
messaging.message.body.size = tracing::field::Empty,
messaging.rocketmq.message.keys = tracing::field::Empty,
);
if let Some(message) = context_plan.parent_message {
if let Err(error) =
crate::propagation::set_span_parent_from_properties_with_handle(telemetry, &span, message.properties)
{
crate::propagation::record_span_parent_assignment_error(telemetry, "client.consumer.process", error);
}
}
{
use tracing_opentelemetry::OpenTelemetrySpanExt;
for link in context_plan.links {
span.add_link(link);
}
}
if let Some(message) = context_plan.first_message {
super::record_message_properties_with_handle(telemetry, &span, message.properties, message.body_size);
}
span
}
#[cfg(not(feature = "otel-traces"))]
{
let _ = telemetry;
for message in messages {
let _ = (message.properties, message.body_size);
}
let _ = (message_count, consumer_group, message_queue, consume_mode);
tracing::Span::none()
}
}
pub fn record_process_event(span: &tracing::Span, event: &'static str, status: &str, message_count: usize) {
#[cfg(feature = "otel-traces")]
{
let _entered = span.enter();
tracing::info!(
target: "rocketmq_observability",
messaging_system = "rocketmq",
messaging_operation_name = "process",
rocketmq_consumer_event = event,
rocketmq_consumer_status = status,
messaging_batch_message_count = message_count as i64,
);
}
#[cfg(not(feature = "otel-traces"))]
{
let _ = (span, event, status, message_count);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consumer_process_span_accepts_empty_batch_without_traces() {
let queue = MessageQueue::from_parts("topic-a", "broker-a", 1);
let span = consumer_process_span(
&TelemetryHandle::noop(),
std::iter::empty(),
0,
"group-a",
&queue,
"concurrently",
);
record_process_event(&span, "complete", "success", 0);
}
#[cfg(feature = "otel-traces")]
#[test]
fn producer_and_consumer_spans_honor_isolated_handle_policies() {
let mut enabled_config = crate::ObservabilityConfig {
enabled: true,
..crate::ObservabilityConfig::default()
};
enabled_config.traces.enabled = true;
let mut disabled_config = enabled_config.clone();
disabled_config.traces.enabled = false;
#[cfg(feature = "otel-metrics")]
let enabled = TelemetryHandle::active(&enabled_config, None);
#[cfg(not(feature = "otel-metrics"))]
let enabled = TelemetryHandle::active(&enabled_config);
#[cfg(feature = "otel-metrics")]
let disabled = TelemetryHandle::active(&disabled_config, None);
#[cfg(not(feature = "otel-metrics"))]
let disabled = TelemetryHandle::active(&disabled_config);
let queue = MessageQueue::from_parts("topic-a", "broker-a", 1);
tracing::subscriber::with_default(tracing_subscriber::Registry::default(), || {
assert!(producer_send_span(&enabled).id().is_some());
assert!(
consumer_process_span(&enabled, std::iter::empty(), 1, "group-a", &queue, "concurrently")
.id()
.is_some()
);
assert!(producer_send_span(&disabled).id().is_none());
assert!(
consumer_process_span(&disabled, std::iter::empty(), 1, "group-b", &queue, "concurrently")
.id()
.is_none()
);
});
}
#[cfg(feature = "otel-traces")]
#[test]
fn consumer_batch_links_each_unique_valid_remote_context() {
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use cheetah_string::CheetahString;
use opentelemetry::trace::SpanId;
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::trace::SpanData;
use opentelemetry_sdk::trace::SpanExporter;
use tracing_subscriber::layer::SubscriberExt;
#[derive(Clone, Debug, Default)]
struct CollectingSpanExporter {
spans: Arc<Mutex<Vec<SpanData>>>,
}
impl SpanExporter for CollectingSpanExporter {
fn export(&self, batch: Vec<SpanData>) -> impl std::future::Future<Output = OTelSdkResult> + Send {
let spans = Arc::clone(&self.spans);
async move {
spans.lock().expect("span export lock poisoned").extend(batch);
Ok(())
}
}
}
fn properties(traceparent: &str) -> HashMap<CheetahString, CheetahString> {
let mut properties = HashMap::new();
properties.insert(
CheetahString::from_static_str(crate::TRACEPARENT),
CheetahString::from_string(traceparent.to_owned()),
);
properties
}
crate::propagation::install_trace_context_propagators();
let invalid = properties("not-a-traceparent");
let first_parent = properties("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
let duplicate_parent = properties("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
let second_parent = properties("00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01");
let messages = [
MessageSpanContext::new(&invalid, None),
MessageSpanContext::new(&first_parent, None),
MessageSpanContext::new(&duplicate_parent, None),
MessageSpanContext::new(&second_parent, None),
];
let mut config = crate::ObservabilityConfig {
enabled: true,
..crate::ObservabilityConfig::default()
};
config.traces.enabled = true;
config.traces.propagate_context = true;
#[cfg(feature = "otel-metrics")]
let telemetry = TelemetryHandle::active(&config, None);
#[cfg(not(feature = "otel-metrics"))]
let telemetry = TelemetryHandle::active(&config);
let exporter = CollectingSpanExporter::default();
let tracer_provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();
let subscriber =
tracing_subscriber::Registry::default().with(crate::trace::build_tracing_layer(&config, &tracer_provider));
let queue = MessageQueue::from_parts("topic-a", "broker-a", 1);
tracing::subscriber::with_default(subscriber, || {
let span = consumer_process_span(&telemetry, messages, 4, "group-a", &queue, "concurrently");
let _entered = span.enter();
});
tracer_provider.force_flush().expect("trace flush failed");
let spans = exporter.spans.lock().expect("span export lock poisoned");
let process_span = spans
.iter()
.find(|span| span.name == CONSUMER_PROCESS)
.expect("consumer process span was not exported");
let first_parent_id = SpanId::from_hex("00f067aa0ba902b7").expect("valid first span id");
let second_parent_id = SpanId::from_hex("bbbbbbbbbbbbbbbb").expect("valid second span id");
let linked_parent_ids = process_span
.links
.iter()
.map(|link| link.span_context.span_id())
.collect::<Vec<_>>();
assert_eq!(process_span.parent_span_id, first_parent_id);
assert!(process_span.parent_span_is_remote);
assert_eq!(linked_parent_ids, vec![first_parent_id, second_parent_id]);
}
}