You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue was written by Claude (Anthropic's Claude Code) under the direction of @lukebakken, who reviewed it before filing. The code references and line numbers were verified against main, but the design options below are AI-drafted starting points, not settled decisions — treat them as such.
Background
#1731 asked for an Activity per connection attempt, with the exception recorded on failure. #1734 delivered that: a RabbitMQ.Client.Connection activity source emitting a connection attempt span per CreateConnectionAsync, a child tcp connection attempt span per endpoint tried, and a separate connection attempt span tagged is_reconnection=true for auto-recovery (AutorecoveringConnection.Recovery.cs).
That issue's text also said "This also applies to auto-recovering channels." The connection half of recovery is instrumented; the topology recovery half is not. Filing separately rather than holding #1731 open, because the span shape here needs design work that the connection case did not.
Current state
TryRecoverConnectionDelegateAsync (AutorecoveringConnection.Recovery.cs:254) is the only recovery method with an Activity. Everything that happens after the socket is re-established is untraced:
TryPerformAutomaticRecoveryAsync (:180) — the orchestrator; runs the documented 4-step sequence (exchanges, queues, bindings, consumers) plus channel recovery
RecoverExchangesAsync (:298)
RecoverQueuesAsync (:350)
RecoverBindingsAsync (:464)
RecoverConsumersAsync (:516)
RecoverChannelsAndItsConsumersAsync (:603)
AutorecoveringChannel.AutomaticallyRecoverAsync (AutorecoveringChannel.cs:153) — re-opens the channel, replays BasicQos / TxSelect, then recovers that channel's consumers
So from a telemetry consumer's point of view, a recovery currently looks like a single connection attempt span that succeeds, followed by silence — even though the interesting failures happen afterwards. Topology recovery is exactly where recovery goes wrong in practice: a queue that no longer exists, a binding to a deleted exchange, a consumer whose queue was removed. Those go to HandleTopologyRecoveryException and the ConnectionRecoveryError / TopologyRecovery* event handlers, and are invisible to a tracing backend.
The design problem
The reason this is not a mechanical copy of the connection instrumentation: cardinality. A single recovery can fan out to hundreds of spans — one connection has up to 2047 channels, and each recorded exchange, queue, binding, and consumer is recovered in its own loop iteration. Naively wrapping every Recover* loop body in an activity would make a recovery on a large topology dominate the trace and could be a real cost during an outage, when recovery is retried on an interval.
Options worth weighing:
One span for the whole topology recovery, child of the connection attempt span, with counts as tags (exchanges.recovered, queues.recovered, bindings.recovered, consumers.recovered, and the corresponding .failed counts). Cheapest, bounded, and probably answers "did recovery fully succeed?" — which is the actual operational question.
A span per phase (exchanges / queues / bindings / channels+consumers), 4-5 spans per recovery, each with counts. Still bounded; pinpoints which phase failed.
A span per recovered entity. Highest fidelity, unbounded cardinality. If offered at all it should be opt-in via RabbitMQTracingOptions, not default-on.
Events rather than spans on the parent recovery span for individual failures — records which entity failed without a span per entity. Composes with 1 or 2.
My leaning is 2 + 4: per-phase spans with counts, and an exception event per failed entity on the enclosing phase span. But this needs a decision, not just an implementation.
Additional questions to settle
Parentage. Recovery is driven by an internal recovery loop task, not by user code, so there is no meaningful ambient parent. Should the topology spans be children of the is_reconnection=trueconnection attempt span (which is currently using-disposed at the end of TryRecoverConnectionDelegateAsync, i.e. before topology recovery even starts), or should the parent span be widened to span the whole recovery? Widening it changes the meaning of the existing span's duration, so this is a compatibility consideration if it lands after 7.3.0 ships.
Status semantics.feat: add activity on connection #1734's review found that recording an exception without also calling SetStatus(ActivityStatusCode.Error) leaves the span looking successful to backends. Same trap applies here, with an extra wrinkle: topology recovery deliberately tolerates some failures (TopologyRecoveryFilter, TopologyRecoveryExceptionHandler). A handled-and-tolerated failure arguably should not mark the span Error. Needs an explicit rule.
Per-channel recovery. Should AutomaticallyRecoverAsync get its own span? It does real broker round-trips (channel.open, up to two basic.qos, optionally tx.select) and is the natural parent for that channel's consumer recovery. This is per-channel, so cardinality is bounded by RequestedChannelMax.
New tag names. The connection spans already needed client-specific tags (messaging.rabbitmq.connection.is_reconnection, messaging.rabbitmq.connection.automatic_recovery) because the OpenTelemetry messaging conventions do not cover connection establishment — they cover recovery even less. Any new tags are effectively our own convention and should be named deliberately.
Testing
Tests belong in projects/Test/SequentialIntegration/ (ActivityRecorder installs a process-global ActivityListener; the Integration project runs its tests in parallel, so recorders there see other tests' activities). Toxiproxy-based tests in projects/Test/Integration/TestToxiproxy.cs are the existing way to force a real recovery — that tension will need resolving, most likely by driving recovery from the sequential project.
Review the entire OpenTelemetry tracing implementation #1967 — review of the entire OpenTelemetry tracing implementation; should land first, since it may change the tracing public surface and establish the guard/status conventions this work would follow
Note
This issue was written by Claude (Anthropic's Claude Code) under the direction of @lukebakken, who reviewed it before filing. The code references and line numbers were verified against
main, but the design options below are AI-drafted starting points, not settled decisions — treat them as such.Background
#1731 asked for an
Activityper connection attempt, with the exception recorded on failure. #1734 delivered that: aRabbitMQ.Client.Connectionactivity source emitting aconnection attemptspan perCreateConnectionAsync, a childtcp connection attemptspan per endpoint tried, and a separateconnection attemptspan taggedis_reconnection=truefor auto-recovery (AutorecoveringConnection.Recovery.cs).That issue's text also said "This also applies to auto-recovering channels." The connection half of recovery is instrumented; the topology recovery half is not. Filing separately rather than holding #1731 open, because the span shape here needs design work that the connection case did not.
Current state
TryRecoverConnectionDelegateAsync(AutorecoveringConnection.Recovery.cs:254) is the only recovery method with anActivity. Everything that happens after the socket is re-established is untraced:TryPerformAutomaticRecoveryAsync(:180) — the orchestrator; runs the documented 4-step sequence (exchanges, queues, bindings, consumers) plus channel recoveryRecoverExchangesAsync(:298)RecoverQueuesAsync(:350)RecoverBindingsAsync(:464)RecoverConsumersAsync(:516)RecoverChannelsAndItsConsumersAsync(:603)AutorecoveringChannel.AutomaticallyRecoverAsync(AutorecoveringChannel.cs:153) — re-opens the channel, replaysBasicQos/TxSelect, then recovers that channel's consumersgrep -n Activity projects/RabbitMQ.Client/Impl/AutorecoveringChannel.csreturns nothing today.So from a telemetry consumer's point of view, a recovery currently looks like a single
connection attemptspan that succeeds, followed by silence — even though the interesting failures happen afterwards. Topology recovery is exactly where recovery goes wrong in practice: a queue that no longer exists, a binding to a deleted exchange, a consumer whose queue was removed. Those go toHandleTopologyRecoveryExceptionand theConnectionRecoveryError/TopologyRecovery*event handlers, and are invisible to a tracing backend.The design problem
The reason this is not a mechanical copy of the connection instrumentation: cardinality. A single recovery can fan out to hundreds of spans — one connection has up to 2047 channels, and each recorded exchange, queue, binding, and consumer is recovered in its own loop iteration. Naively wrapping every
Recover*loop body in an activity would make a recovery on a large topology dominate the trace and could be a real cost during an outage, when recovery is retried on an interval.Options worth weighing:
connection attemptspan, with counts as tags (exchanges.recovered,queues.recovered,bindings.recovered,consumers.recovered, and the corresponding.failedcounts). Cheapest, bounded, and probably answers "did recovery fully succeed?" — which is the actual operational question.RabbitMQTracingOptions, not default-on.My leaning is 2 + 4: per-phase spans with counts, and an exception event per failed entity on the enclosing phase span. But this needs a decision, not just an implementation.
Additional questions to settle
is_reconnection=trueconnection attemptspan (which is currentlyusing-disposed at the end ofTryRecoverConnectionDelegateAsync, i.e. before topology recovery even starts), or should the parent span be widened to span the whole recovery? Widening it changes the meaning of the existing span's duration, so this is a compatibility consideration if it lands after 7.3.0 ships.SetStatus(ActivityStatusCode.Error)leaves the span looking successful to backends. Same trap applies here, with an extra wrinkle: topology recovery deliberately tolerates some failures (TopologyRecoveryFilter,TopologyRecoveryExceptionHandler). A handled-and-tolerated failure arguably should not mark the spanError. Needs an explicit rule.AutomaticallyRecoverAsyncget its own span? It does real broker round-trips (channel.open, up to twobasic.qos, optionallytx.select) and is the natural parent for that channel's consumer recovery. This is per-channel, so cardinality is bounded byRequestedChannelMax.messaging.rabbitmq.connection.is_reconnection,messaging.rabbitmq.connection.automatic_recovery) because the OpenTelemetry messaging conventions do not cover connection establishment — they cover recovery even less. Any new tags are effectively our own convention and should be named deliberately.Testing
Tests belong in
projects/Test/SequentialIntegration/(ActivityRecorderinstalls a process-globalActivityListener; theIntegrationproject runs its tests in parallel, so recorders there see other tests' activities). Toxiproxy-based tests inprojects/Test/Integration/TestToxiproxy.csare the existing way to force a real recovery — that tension will need resolving, most likely by driving recovery from the sequential project.Related