Skip to content

Add tracing activities for topology and channel recovery #1970

Description

@lukebakken

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 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

grep -n Activity projects/RabbitMQ.Client/Impl/AutorecoveringChannel.cs returns nothing today.

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:

  1. 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.
  2. A span per phase (exchanges / queues / bindings / channels+consumers), 4-5 spans per recovery, each with counts. Still bounded; pinpoints which phase failed.
  3. A span per recovered entity. Highest fidelity, unbounded cardinality. If offered at all it should be opt-in via RabbitMQTracingOptions, not default-on.
  4. 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=true connection 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.

Related

Metadata

Metadata

Assignees

Labels

A-observabilityArea: Metrics, counters, and logging.A-opentelemetryArea: OpenTelemetry tracing package.A-recoveryArea: Automatic connection and topology recovery.C-enhancementCategory: Improvements

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions