Skip to content

feat(otel): Add OpenTelemetry distributed tracing support#1769

Open
jbonofre wants to merge 1 commit intoapache:mainfrom
jbonofre:add-opentelemetry-support
Open

feat(otel): Add OpenTelemetry distributed tracing support#1769
jbonofre wants to merge 1 commit intoapache:mainfrom
jbonofre:add-opentelemetry-support

Conversation

@jbonofre
Copy link
Copy Markdown
Member

New activemq-opentelemetry module that instruments the broker using the OpenTelemetry API. The plugin traces send (PRODUCER), dispatch (CONSUMER), and acknowledge (INTERNAL) operations with W3C TraceContext propagation and standard OTel messaging semantic conventions.

Depends on opentelemetry-api only; users bring their own SDK and exporter at runtime. Included in the distribution with example configuration. Maybe we should add a default SDK/exporter in the ActiveMQ distribution, just wondering 😄

@jbonofre jbonofre requested a review from jeanouii March 13, 2026 16:34
@jbonofre jbonofre linked an issue Mar 13, 2026 that may be closed by this pull request
@jbonofre jbonofre changed the title Add OpenTelemetry distributed tracing support feat(otel): Add OpenTelemetry distributed tracing support Mar 13, 2026
New activemq-opentelemetry module that instruments the broker using the
OpenTelemetry API. The plugin traces send (PRODUCER), dispatch (CONSUMER),
and acknowledge (INTERNAL) operations with W3C TraceContext propagation
and standard OTel messaging semantic conventions.

Depends on opentelemetry-api only; users bring their own SDK and exporter
at runtime. Included in the distribution with example configuration.
@jbonofre jbonofre force-pushed the add-opentelemetry-support branch from 0e1e69d to 176f5f5 Compare March 26, 2026 05:08
Copy link
Copy Markdown
Contributor

@jeanouii jeanouii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great PR and a good start.
I've added a couple of notes and questions.

It's been a while since I worked with this.

@Override
public void postProcessDispatch(MessageDispatch messageDispatch) {
if (messageDispatch != null) {
Span span = dispatchSpans.remove(messageDispatch);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because by nature, this can't be done in a try/finally block, some spans may leak in the map forever.
The map needs to be either bound or we need to implement a TTL to remove old entries in my opinion.

}
if (producerExchange.getConnectionContext() != null
&& producerExchange.getConnectionContext().getClientId() != null) {
spanBuilder.setAttribute("messaging.client_id", producerExchange.getConnectionContext().getClientId());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use client_id a lot, but naming conventions require . (dot)

SpanBuilder spanBuilder = tracer.spanBuilder(destinationName + " publish")
.setSpanKind(SpanKind.PRODUCER)
.setParent(parentContext)
.setAttribute("messaging.system", "activemq")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"messaging.system" --> "messaging.system.name"?

.setParent(parentContext)
.setAttribute("messaging.system", "activemq")
.setAttribute("messaging.destination.name", destinationName)
.setAttribute("messaging.operation", "publish");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"messaging.operation" --> "messaging.operation.type"?

}

TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
Tracer tracer = GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these 2 lines be done in the constructor only and not for each invocation?

public void set(Message message, String key, String value) {
try {
message.setProperty(key, value);
message.setMarshalledProperties(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to force the properties to be injected, correct?
what's the performance impact?

producer.close();

// Allow async broker processing to complete
Thread.sleep(500);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the WaitFor pattern instead?

Context extractedContext = propagator.extract(Context.current(), message, ActiveMQMessageTextMapGetter.INSTANCE);

String destinationName = message.getDestination().getPhysicalName();
SpanBuilder spanBuilder = tracer.spanBuilder(destinationName + " deliver")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this going to show up for topics?
Many spans for the same message?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's multiple messages per topic here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OpenTelemetry plugin

2 participants