Test Report — Dynatrace OneAgent integration¶
Live validation of the live invocation observers and the
dynatrace-oneagent extension against a Dynatrace sandbox environment, conducted 2026-10-22 and 2026-10-23 from a
single developer machine, with the support of Dynatrace's engineering team. The subject is the OneAgent lab
(lab/dynatrace-oneagent): two Mercury applications and a Kafka broker in containers, with OneAgent attached. This
report records what was run, the evidence, the findings each iteration produced, and what remains.
No environment address, token or tenant identifier appears in this report or in any committed file. The OneAgent installer, which is commercially licensed, is ignored by git and never committed.
Summary¶
One Mercury request is one connected Dynatrace trace, from the REST entry through every composable function, across Kafka into a second application, and back to the HTTP response. The Dynatrace trace id equals Mercury's trace id, Mercury's first span is a child of OneAgent's HTTP node, and Mercury's own span lineage is unbroken in both applications. The maintainer judged the implementation ready to deploy to the field.
| Claim | Evidence | Result |
|---|---|---|
Every hop of a request is in one PurePath, the RPC and the Mono included |
Dynatrace waterfalls | PASS |
| The trace crosses Kafka into the consuming application | Dynatrace waterfalls; A-B-A on dt.trace_id in the logs |
PASS |
The Dynatrace trace id is Mercury's trace id, with or without a client traceparent |
Log lines (trace_id = dt.trace_id); Dynatrace |
PASS |
| Mercury's edge span is a child of OneAgent's server node | Dynatrace span id against Mercury's parent_span_id |
PASS |
| Mercury's own telemetry has no broken spans | Span-by-span check of the Telemetry log, three orders | PASS |
| The negative control breaks one link and only one | Dynatrace; log lines | PASS |
| No OneAgent SDK warnings | Application logs | PASS |
| Each hop is named after its application and route, in one service | Dynatrace request list | PASS |
/info reports the application's own version under OneAgent |
/info |
PASS |
The system under test¶
| Element | Value |
|---|---|
| Machine | macOS developer machine, Apple silicon |
| Container runtime | Colima, an aarch64 Ubuntu 24.04.5 LTS VM with the Docker runtime |
| Agent | Dynatrace OneAgent 1.347.64, installed full stack in Colima's VM (signature verified), injected into every JVM a container starts |
| Images | Ubuntu 26.04.1 LTS with OpenJDK 25.0.4.1 (lab/dynatrace-oneagent/base) |
| Mercury | branch feature/dynatrace-oneagent-compatibility (PR #583), version 4.12.25, OneAgent SDK 1.9.0 |
| Broker | helpers/kafka-standalone |
Three containers share one network namespace, because the broker advertises 127.0.0.1:9092:
lab-edge, the REST entry point.POST /api/lab/ordersruns thelab-orderflow:lab.validate, which makes an RPC tolab.lookupfrom inside the function;lab.price, which returns aMonothat completes 200 ms later on a Reactor thread;lab.audit, the negative control (invocation.observer.unlinked);simple.kafka.notificationto the topiclab.orders; andlab.receipt.lab-worker, the Kafka entry point. Theminimalist-kafkaflow adapter consumeslab.ordersand runslab-fulfil:lab.reserve, thenlab.ship.lab-broker.
The settings under test, in both applications:
dynatrace.oneagent.enabled=true
dynatrace.oneagent.destination=application-route
dynatrace.oneagent.log.context=true
log.format=compact
invocation.observer.unlinked=lab.audit
cd lab/dynatrace-oneagent
./lab.sh build && ./lab.sh up oneagent # ./lab.sh up none for the baseline
./lab.sh order # one order; repeat
./lab.sh lineage # the span check of Mercury's own telemetry (Scenario 9)
Scenario 1 — the baseline: OneAgent without the adapter¶
With OneAgent attached and the adapter off, each request was one span (/api/lab/orders, about 230 ms) under a
generic Netty service, with no child spans: the hops between composable functions were invisible, because Mercury hands
an event through its event bus and a per-route queue, where an agent's thread-bound context is lost. Mercury's own
trace id still crossed Kafka.
Scenario 2 — every hop in one PurePath¶
With the adapter on, each request became one connected, nested PurePath from server /api/lab/orders: a producer
and consumer pair per hop, the RPC from inside lab.validate, and the Mono hop continued by OneAgent's own Reactor
support (reactor.core.scheduler.WorkerTask). Every function's log line carries dt.trace_id and dt.span_id, and
every node carries the captured attributes mercury.route and mercury.trace_id, so a Mercury trace id is searchable
in Dynatrace with no environment setup.
Scenario 3 — across Kafka, proven A-B-A¶
simple.kafka.notification carries the observers' tags as x-mercury-tag-<tag> headers, and the flow adapter opens
each message as one incoming hop on kafka.flow.adapter. The check reads the log lines: the worker's functions are
linked when their dt.trace_id is the edge's.
| Leg | Edge setting | Worker's Dynatrace trace |
|---|---|---|
| A | normal | the edge's, 3 of 3 orders |
| B | lab.orders excluded from observation, so no tag is exported |
its own, 3 of 3 |
| A | normal again | the edge's, 3 of 3 |
Leg B is the experiment that makes the other two mean something: a link that cannot be shown to break proves nothing.
Scenario 4 — the negative control isolates its own route¶
invocation.observer.unlinked makes a route start a new trace, so a run can show that a missing link is visible. In
the first run it moved the rest of the flow into its fragment, because the route's reply carried a tag of its new
trace. The route's reply now keeps the caller's tag, and the route's scope closes before the reply leaves. For 3 of 3
orders, the request is one trace and lab.audit alone the other.
Withholding the tag alone changed nothing. task.executor runs on an executor task submitted from the replying thread,
and OneAgent carries that thread's active PurePath into the task; an SDK tracer started there ignores its tag. Ending
the route's path before the handoff is what breaks the link. With no unlinked route, each Mercury trace is exactly one
Dynatrace trace across both applications (3 of 3).
Scenario 5 — one trace id, one lineage¶
A request that carries a W3C traceparent keeps it, and OneAgent continues the same one: the ids agreed end to end
(4bf92f35…, 0af76519…). A request without one now continues the agent's trace context (InvocationObserver.traceparent)
at the REST edge, and a Kafka message without one at the flow adapter, as if the caller had sent it.
- With no client
traceparent, Mercury's trace id equals the Dynatrace trace id in both applications, 3 of 3 orders. - Confirmed in the Dynatrace UI: in trace
6f9157b5c7a6e46ba1f43a79ef51ccd5, the server span of/api/lab/ordersisa8b280c1ddc4af29, the parent that Mercury recorded for its edge span. The maintainer's reading of that trace:http.flow.adapter, the edge's hops andsimple.kafka.notification, thenkafka.flow.adapter,event.script.managerand the worker's hops, then back toasync.http.response, in one tree.
Scenario 6 — no SDK warnings on a deferred result¶
The first runs logged "incoming inProcessLink received on already started path ... is being lost" twice per order: the
adapter linked a Mono completion to its hop, but OneAgent's Reactor support had already continued the path onto the
completion thread. The link is now started only when the completion thread is not already in the hop's trace. Result:
0 SDK warnings in either application across every later run, with the Mono hop still in the trace.
Scenario 7 — each hop named after its application and route¶
The endpoint of an SDK message tracer is <destination type>: <destination name>, and the type is a queue or a topic,
so the prefix cannot be dropped from the one tracer kind that carries the linkage tag. Every hop's destination is now a
topic, the Kafka term, named <application> - <route> by default. The request list in Dynatrace shows
Topic: lab-edge - lab.validate, Topic: lab-worker - lab.reserve and so on, all under one service, "Mercury
Messaging", so per-route names add no service (orders 5b95a65b…, bb3d424c…, 985653c8…).
Scenario 8 — the application's own version under OneAgent¶
Under full-stack OneAgent, the startup log said "Application name: oneagentjava.17" and /info reported version
1.347.64.20261021-162634: the version came from the class loader's first META-INF/MANIFEST.MF, and OneAgent's jar
on the boot class path is searched first. Fixed on main by #585. The lab, rebuilt with it, reports lab-edge 4.12.25.
Scenario 9 — Mercury's own telemetry has no broken spans¶
The Dynatrace tree includes OneAgent's plumbing nodes, such as the EventEmitter$$Lambda executor tasks. Mercury's
Telemetry log records only its own spans: the REST edge, the composable functions, task.executor and
async.http.response. To check that its lineage is whole, both applications' logs were saved and every Telemetry
record of the three most recent orders was linked by span_id and parent_span_id (./lab.sh lineage).
| Order (Mercury trace id) | Span records | Roots | Unreachable | Links across Kafka | Result |
|---|---|---|---|---|---|
d0c236ae8fa7e618cb5f3a57ab7bd22c |
14 (10 edge, 4 worker) | 1 | 0 | 2 | CONNECTED |
589cea5ad70b1930ccb25f05aa3016e3 |
14 (10 edge, 4 worker) | 1 | 0 | 2 | CONNECTED |
e7e917c6e165f9fb3163c9f8e665e269 |
14 (10 edge, 4 worker) | 1 | 0 | 2 | CONNECTED |
The check was first shown to fail: with one lab.price record removed from a copy of the log, it found two roots and
reported the order BROKEN (exit code 1). The one root is the REST edge, http.request, whose parent is OneAgent's
server node and no Mercury span. No span id is
repeated. Each order has one record without a span id: the flow adapter's RPC round-trip record for event.script.manager,
attached to a valid parent. It has no span of its own by design (#215): the RPC inbox records the responder's span id
only when the route it called also replied, and a flow replies from its last task. On the log lines, each order is the
same two Dynatrace traces: the request, with Mercury's trace id, and lab.audit alone. One of the three trees:
http.request [lab-edge] + 0.0 ms 243.46 ms root; parent 4448bdda80e074a2 is OneAgent's server node
http.flow.adapter [lab-edge] + 3.0 ms
task.executor [lab-edge] + 5.0 ms
lab.validate [lab-edge] + 7.0 ms
lab.lookup [lab-edge] + 8.0 ms (the RPC from inside lab.validate)
lab.price [lab-edge] + 13.0 ms (the Mono; its successor starts about 200 ms later)
lab.audit [lab-edge] +219.0 ms
simple.kafka.notification [lab-edge] +222.0 ms
lab.receipt [lab-edge] +241.0 ms
async.http.response [lab-edge] +242.0 ms
task.executor [lab-worker] +244.0 ms crosses Kafka
lab.reserve [lab-worker] +245.0 ms crosses Kafka
lab.ship [lab-worker] +248.0 ms
event.script.manager [lab-worker] RPC round-trip record, no span of its own
lab.audit is a normal span in Mercury's own tree: the negative control acts only on the observers' tags.
Scenario 10 — OneAgent off: the original design¶
With the switch off, or with no agent attached (the SDK reports itself inactive and the observer is not registered),
the observer list is empty: every hook returns after one emptiness check, the trace id is chosen as before (a
traceparent, then the trace-id header, then a fresh id), no x-mercury-tag-* header is written, and replies go out
unchanged. Mercury's Telemetry log is identical to before. The whole reactor, whose telemetry and W3C tests run with no
observer registered, passed after the merge of main: BUILD SUCCESS, 39 of 39 modules, 2,772 tests, 0 failures, 3
skipped, all coverage gates met.
Findings and their fixes¶
| Finding | Resolution |
|---|---|
dynatrace.oneagent=true was lost beside dynatrace.oneagent.*: a configuration key holds a value or child keys, never both |
The switch is dynatrace.oneagent.enabled. An audit of every key found the same collision in the mini-scheduler, fixed by #584 |
/info reported OneAgent's version |
ManifestReader reads the application's own jar (#585) |
| The negative control moved the rest of the flow into its fragment | The route's reply keeps the caller's tag, and its scope closes before the reply leaves (Scenario 4) |
| No link across Kafka | The tags travel as x-mercury-tag-* headers; one hop per message (Scenario 3) |
Two SDK warnings per order on the Mono hop |
The link is skipped where OneAgent already continued the path (Scenario 6) |
Two trace ids per request without a client traceparent |
The agent's traceparent is continued at the REST edge and the Kafka consumer (Scenario 5) |
Every hop was Queue: <application> |
Topic: <application> - <route> by default, one service (Scenario 7) |
| A lab left running answered the reactor tests' requests on port 8100 | The lab publishes host port 18100 |
| The lab in the root reactor would be registered by the field's static code analysis | The lab has its own aggregator, lab/dynatrace-oneagent/pom.xml, and the root pom does not list it |
The OpenTelemetry API adapter was built and removed. Under OneAgent its spans were recorded, named after their
routes, but every hop through the event bus became a separate request. A diagnostic build showed why: OneAgent links an
API span only through its own context on the thread and ignores a W3C parent set by the caller (SERVER spans behave
the same; INTERNAL spans are not created at all), and its PurePath is not OpenTelemetry's current context at the REST
entry. The observer seam stays vendor-neutral, so an adapter for an agent that honours W3C parents can come later.
What remains¶
- Overhead was not measured. The lab checks correctness, not cost; a load comparison with the adapter on and off is still to do.
- A Kafka message without any trace continuing the agent's context is covered by unit tests only: a Mercury
publisher always sends a
traceparent, so the lab does not produce one. - Not exercised:
- Event-over-HTTP between applications. The tag is an envelope tag, which Event-over-HTTP serializes with the envelope, but no test or lab run has driven the linkage across it.
Fluxstreams, which the adapter does not cover.- OneAgent's own log enrichment combined with
dynatrace.oneagent.log.context, which the guide advises against. - The field. The maintainer judged the implementation ready to deploy; a field installation's own run is the next evidence.