Outbound Events
Outbound events are messages your monitored service publishes back to the CritterWatch console. They flow over your existing Wolverine transport from the service to the console's critterwatch queue.
This page is for integrators who want to consume the same telemetry from their own infrastructure (a separate analytics pipeline, a custom dashboard, an audit system, etc.). For day-to-day operation, you don't need any of this — the console handles it for you.
How telemetry is shaped
Most outbound traffic is wrapped in a single ServiceUpdates batch published once per second. A few message types are published on demand in response to specific console queries (handler source code, HTTP chain source code, pending-migration probe results, tenant list refresh).
The batch carries everything CritterWatch needs to render a service's pages in one round-trip — endpoint snapshot, subscription catalog, persistence counts, shard states, agent health, and any change events that occurred since the previous batch.
What's in ServiceUpdates
public record ServiceUpdates(
string ServiceName,
string Label,
string WolverineVersion,
EndpointState[] Endpoints,
MessagingSubscription[] Subscriptions,
WolverineChange[] Changes,
AgentHealthReport[] AgentHealth,
PersistenceCounts PersistenceCounts,
ShardStateSnapshot[] ShardStates
);The Changes array carries the discrete state-change events that occurred since the previous batch. The other arrays are full snapshots — current endpoints, current subscriptions, current health.
Each ShardStateSnapshot carries an optional TenantId. On a single-tenant service it's always null. On a service using per-tenant projection partitioning, ShardStates contains one entry per (shard, tenant) pair, and the console aggregates them into the per-tenant view on the Projections page. Legacy services that haven't enabled per-tenant partitioning continue emitting nulls and look unchanged in the UI.
Change-event categories
The WolverineChange discriminator covers:
| Category | Trigger | Drives |
|---|---|---|
NodeAdded / NodeRemoved | Process node joined or left the cluster | Cluster tab → Nodes view |
LeadershipChanged | New leader elected | Cluster tab → Leader pill |
AgentStarted / AgentStopped | Agent assignment changes | Cluster tab → Agents view |
EndpointAdded / EndpointStopped | Listener or sender lifecycle | Endpoints tab |
CircuitBreakerTripped / CircuitBreakerReset | Endpoint resilience event | Endpoints tab + alert |
BackPressureTriggered / BackPressureLifted | Flow-control event | Endpoints tab + alert |
ExceptionTriggered | Handler exception | Timeline + DLQ rate metrics |
On-demand response messages
These are published in response to a specific console query rather than on the regular cadence:
| Response | Triggered by | Carries |
|---|---|---|
HandlerSourceCodeResponse | RequestHandlerSourceCode | Generated handler source code for a message type |
HttpChainSourceCodeReported | RequestHttpChainSourceCode | Generated source code for an HTTP chain |
HttpChainOpenApiReported | RequestHttpChainOpenApi | OpenAPI descriptor for one HTTP chain (lazy-fetched on demand) |
EndpointPropertiesReported | RequestEndpointProperties | Per-endpoint Properties / Children config tree (lazy-fetched when the Pipeline tab opens an endpoint) |
MessageHandlerPropertiesReported | RequestMessageHandlerProperties | Per-handler Properties rows for one message type (lazy-fetched on the handler-chain detail page) |
PendingMigrationsReportedResponse | RequestPendingMigrationsCheck | Pending EF Core migration list for a DbContext |
TenantListResponse | RequestTenantList | Current tenant list for multi-tenant services (one row per (tenantId, sourceName) when the service has multiple IDynamicTenantSource registrations) |
AgentHealthReport | RequestAgentHealthReport (or scheduled) | Agent health snapshot |
RestartProjectionResult | RestartProjection / RebuildProjection | Success/failure of the operation |
ScheduledMessageEdited | EditScheduledMessage | Confirmation of edit + new execution time |
StaleNodesEjected | EjectNode | Confirmation of ejected node ids |
SubscriptionOrProjectionRestarted | Auto-restart on stall | Why and when |
The three lazy-fetched responses (HttpChainOpenApiReported, EndpointPropertiesReported, MessageHandlerPropertiesReported) carry the bulky descriptor substructures that used to ship with every ServiceUpdates snapshot. They moved off the heartbeat path so the snapshot stays under the SQS / broker payload cap on big topologies — see Observer → Lazy-fetched detail panes.
Capability advertisement (on startup)
The first ServiceUpdates batch after startup carries the full capability set:
MessagingSubscription[]— every message type with its handler binding and routing role (Handler / Publisher).EndpointState[]— every listener and sender with full configuration.MessageStoreDiscovered— every Wolverine durability store with its database URI and store type (Postgresql/SqlServer/InMemory).- Event-store metadata (Marten / Polecat).
- Tenancy mode + current tenant list.
- Wolverine version string.
This snapshot is re-issued whenever the Wolverine runtime is reinitialized. It replaces the prior shape wholesale — no merge logic.
Causation discovery
When the runtime observes that handling message A resulted in publishing message B, the service publishes a MessageCausationDiscovered event. The console aggregates these to draw the causation graph on the Message Topology page.
Causation is reported per discovered pair, not on every occurrence — once a causation is known, repeated observations don't re-publish.
Message shape on the wire
Telemetry uses Wolverine's standard JSON envelope on the transport. When consumed via SignalR (e.g., by the browser), messages use the CloudEvents wrapper:
{
"type": "service_updated",
"data": {
"serviceName": "trip-service",
...
}
}The type discriminator is snake_case derived from the C# type name. Properties inside data are camelCase. Enums serialize as string names.
Consuming telemetry from your own infrastructure
If you want to subscribe to the same telemetry stream from a separate process (analytics, custom audit, etc.):
- Listen on the
critterwatchRabbitMQ queue alongside the console — or have your service fan out to a second queue using Wolverine's transport routing. - Reference
Wolverine.CritterWatchand deserialize the message types directly. - Treat
ServiceUpdatesas a complete snapshot — don't try to reconstruct state from theChangesarray alone, because some change events depend on having seen the prior snapshot.
For analytics pipelines, the high-volume surface is ServiceUpdates (one per second per service). The bursty / on-demand surface is the response messages and the alert lifecycle events emitted by the console (which are republished over SignalR for the browser; you can subscribe to that hub if your consumer is a server-side SignalR client).
