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 class ServiceUpdates : ISerializable, ICritterWatchMessage
{
// ONLY set on the first send
public ServiceCapabilities? Capabilities { get; set; }
public string Id { get; }
public int NodeNumber { get; }
public string Version { get; }
public Uri CommunicationUri { get; }
public ServiceUpdates(string id, int nodeNumber, string version, Uri communicationUri)
{
Id = id;
NodeNumber = nodeNumber;
Version = version;
CommunicationUri = communicationUri;
}
public EndpointDescriptor[] Endpoints { get; set; } = [];
public MessagingSubscription[] Subscriptions { get; set; } = [];
public WolverineChange[] Changes { get; set; } = [];
public StaleNodesEjected[] NodesEjected { get; set; } = [];
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
public PersistenceCountsUpdated[] PersistenceCounts { get; set; }
public MessageHandlingMetrics[] MessagingMetrics { get; set; }
public ShardStateSnapshot[] ShardStates { get; set; } = [];
/// <summary>
/// #476 Part 2 — per-store authoritative shard-identity sets for stale-shard
/// reconciliation. One entry per registered event store per poll; CritterWatch
/// prunes any persisted shard it still holds for a store that is absent from
/// the store's <see cref="ShardReconciliationMarker.KnownShardIdentities"/>.
/// Empty on older Wolverine.CritterWatch builds that predate the field, in
/// which case no reconciliation happens and stale orphan cards persist as
/// before.
/// </summary>
public ShardReconciliationMarker[] ShardReconciliations { get; set; } = [];
public AgentHealthReport[] HealthReports { get; set; } = [];
public EndpointHealthReport[] EndpointHealthReports { get; set; } = [];
/// <summary>
/// Operator-declared throughput / exec-time baselines this service is
/// advertising. Sent only on first activation (alongside Capabilities), so
/// CritterWatch can seed its alert thresholds before it has its own
/// historical samples. May be null/empty when no baselines were declared.
/// </summary>
public DeclaredBaseline[] DeclaredBaselines { get; set; } = [];
/// <summary>
/// Snapshot of the service's ASP.NET Core <see cref="IHealthCheck"/>
/// registrations that reference Wolverine (#73). Sent only on first
/// activation alongside <see cref="Capabilities"/>. CritterWatch surfaces
/// the result on the Service Details Overview tab so operators can confirm
/// at a glance whether the service is wired up to k8s liveness/readiness
/// probes. Null when the host hasn't registered any health checks at all.
/// </summary>
public HealthCheckRegistrationsDescriptor? HealthCheckRegistrations { get; set; }
// Serialization plumbing (Write/Read) elided from the docs snippet.The constructor carries the identity fields — Id, NodeNumber, Version, and the node's CommunicationUri. Capabilities is set only on the first send (see Capability advertisement).
The Changes array carries the discrete state-change events that occurred since the previous batch. The other arrays are full snapshots — current endpoints (Endpoints), current subscriptions (Subscriptions), agent and endpoint health (HealthReports / EndpointHealthReports), persistence counts (PersistenceCounts), messaging metrics (MessagingMetrics), and shard states (ShardStates).
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).EndpointDescriptor[]— 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).
