Skip to content

Embedded CritterWatch ​

Run the console inside your own application's Wolverine runtime, instead of as a separate service. It mounts at a path you choose, monitors the host it lives in, and keeps its own data in its own store so nothing of yours is touched.

This is aimed squarely at the development inner loop: you get the console while you are working on the app, with no second process to run and — on SQLite — no infrastructure at all.

When to use the standalone console instead

Embedded monitors its own host and nothing else. If you need one console watching several services, run the standalone console; that is what it is for. The two are the same console, wired differently.

What isolation you get ​

The console's documents, events and schema live in an ancillary document store that is entirely its own. Concretely:

  • Your store is untouched. The console never writes documents or events into it, and never migrates it.

  • Your event store keeps its own stream identity. CritterWatch identifies streams by string; most applications use Guid. That is a per-store setting, not a per-aggregate one, which is exactly why the console needs its own store rather than a corner of yours.

  • The console gets its own wolverine_* envelope tables, in its own schema (or, on SQLite, its own file). One runtime, but two message stores.

    This page previously said the opposite

    Until 1.1 this section claimed the console's messages ride your application's existing envelope tables and that "you do not get a second set of wolverine_* tables to reason about or clean up." That was wrong, and it was measured rather than argued: a test provisions a real host three ways and reads the database catalogue.

    Sharing happens only when the host has set opts.Durability.MessageStorageSchemaName. ⚠️ That is not the same property as the MessageStorageSchemaName you set inside IntegrateWithWolverine(o => ...) — there are two with that name, the ancillary integration reads only the runtime-level one, and setting the other looks like it worked because it does take effect for your own tables.

    What it means for you: the console's envelope tables are a second set, so a maintenance routine that resets or rebuilds only the main message store will not touch them. If you run such a routine, iterate every store rather than IWolverineRuntime.Storage:

    csharp
    foreach (var store in await host.GetRuntime().Stores.FindAllAsync())
    {
        await store.Admin.RebuildAsync();
    }

    A second set left unmanaged is the shape of a bug we have already shipped once, where stale envelopes in an unswept schema replayed a previous run's telemetry on every boot.

Getting started ​

Prerelease

Embedded mode ships in 1.1. Until 1.1.0 is final, use the latest 1.1 prerelease, and no older than 1.1.0-rc.2. The betas carry the embedding defects this page describes as "through 1.1.0-beta.8", and in 1.1.0-rc.1 a command from the browser skipped the console's own checks: see Putting the console behind your authorization.

A complete worked example is critterwatch/Embedded.Sqlite in the CritterStackSamples repository — one ASP.NET Core app with its own documents and handler, the console embedded in it, and no broker, database server or containers.

Two calls on the host you already have — one to register, one to mount:

cs
builder.Host.UseWolverine(opts =>
{
    // ... your own Wolverine configuration ...

    // On PostgreSQL/Marten this takes an NpgsqlDataSource.
    // The Polecat (SQL Server) and Fisher (SQLite) overloads take a connection string.
    opts.AddCritterWatchEmbedded(postgresSource, schemaName: "critterwatch");
});

var app = builder.Build();

// Mounts the console's UI under /critterwatch. Your own routes are untouched.
app.UseCritterWatchEmbedded();

The registration call differs by store

On PostgreSQL/Marten it takes an NpgsqlDataSource. On SQL Server/Polecat and SQLite/Fisher it takes a connection string. AddCritterWatchEmbedded and UseCritterWatchEmbedded also live in different namespaces — CritterWatch.Services and CritterWatch.Services.Hosting.

The console registers its own store, routes its own handlers and endpoints to it, and serves its UI under the path you choose — /critterwatch by default.

Two things the mount deliberately does not do

It does not map health endpoints, and it does not serve the SPA at your application's root. The standalone console does both, because it owns its whole route table; a mounted console is a guest and yours stays yours. Wolverine.HTTP maps its endpoints once per application: if yours calls MapWolverineEndpoints() before UseCritterWatchEmbedded(), the console leaves that mapping alone, and if it calls it afterwards your application fails at startup and says so. Pass mapWolverineEndpoints: false to leave the mapping entirely to your application. The console's own endpoints are mapped by UseCritterWatchEmbedded() either way; they are not among the ones MapWolverineEndpoints() discovers.

The console's UI is served from routes under its mount path, so your application's own SPA fallback (MapFallbackToFile("index.html"), a development-server proxy, a catch-all route) keeps answering your routes and never answers the console's. Everything outside the mount path stays yours. Through 1.1.0-beta.8 the UI was served by fallback middleware instead, so on a host with a fallback of its own every console deep link answered with the host's page.

Putting the console behind your authorization ​

The console's UI, its HTTP API (/api/critterwatch/*) and its SignalR hub (/api/messages) are mapped on your route table, and permissions are opt-in, so anyone who can reach your host can use the console unless you gate it. configureEndpoints is applied to every endpoint the mount creates, and to nothing of yours:

cs
builder.Services.AddAuthorization(o => o.AddPolicy("ops", policy => policy.RequireRole("operations")));

builder.Host.UseWolverine(opts =>
{
    opts.AddCritterWatchEmbedded(postgresSource, schemaName: "critterwatch", configure: console =>
    {
        // The monitored half's options: the ones a standalone service sets on what its own
        // AddCritterWatchMonitoring call returns. Embedded, the console makes that call for you.
        console.ConfigureMonitoring = monitoring =>
        {
            monitoring.EnableSqlQuery = false;
            monitoring.EnableDocumentExplorer = false;
        };
    });
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

// Applied to every endpoint the mount creates: the UI, the console's HTTP API and its SignalR hub.
// Your application's own endpoints keep their own rules.
app.UseCritterWatchEmbedded(configureEndpoints: endpoints => endpoints.RequireAuthorization("ops"));

ConfigureMonitoring reaches the options a standalone service sets on what its own AddCritterWatchMonitoring call returns, including EnableEventStoreExplorer, EnableDocumentExplorer and EnableSqlQuery. Embedded, the console makes that call for you, so this is where you set them. Their defaults are the same as a standalone service's: on in Development, off elsewhere. Many internal and staging environments run as Development, so set them explicitly where that matters.

Permissions and the audit log apply to embedded commands too. Every command the SPA sends passes through the console before your application's handler runs it, so a registered ICritterWatchAuthorizer is asked, the audit log records it, and a service parked over the licensed limit is refused, exactly as on a standalone console. Your handler then runs once.

Fixed in 1.1.0-rc.2

In 1.1.0-rc.1 a command sent from the browser reached your application's handlers without passing through the console, so role-based access control, the audit row, the "command accepted" acknowledgement and the license-cap refusal were all skipped: a replay denied by RBAC went ahead. If you embed the console with permissions turned on, upgrade.

Bearer-token-only hosts

The console's SPA connects to the hub without an access token, so the policy has to be satisfiable by a cookie, Windows authentication or a header your proxy adds. A host that accepts only bearer tokens will refuse the SPA's hub connection.

No runtime compilation ​

The console's handlers and HTTP endpoints ship pre-generated inside the package and are registered directly, so embedding it adds no code generation to your application:

  • Your host does not need WolverineFx.RuntimeCompilation on the console's account. You need it only if your application runs TypeLoadMode.Dynamic for handlers of its own — which is Wolverine's rule for any application, not CritterWatch's.
  • A host with no runtime compiler runs TypeLoadMode.Static, because Wolverine refuses Dynamic without one. Your own dotnet run -- codegen write then covers your handlers only: there are no CritterWatch chains in your application to pre-build.
  • Your handler policies and middleware do not apply to the console's handlers. They are not discovered in your application, so an IHandlerPolicy, AddMiddleware or AutoApplyTransactions() of yours never reaches them. Global failure policies (opts.Policies.OnException…) still combine with the console's own rules, as they do for every handler.

Running without a broker: PostgreSQL queues ​

An embedded console needs no broker of its own — it monitors its own host over in-process local:// messaging. If your application also wants to avoid a broker for its own messaging, Wolverine's database-backed queues run in the PostgreSQL you are already using:

cs
builder.Host.UseWolverine(opts =>
{
    // Wolverine's database-backed queues, in the same PostgreSQL you already run.
    //
    // role: MessageStoreRole.Ancillary is REQUIRED. Without it the transport registers itself
    // as a second "Main" message store, collides with your own Marten IntegrateWithWolverine
    // store, and the application fails to start with:
    //
    //   InvalidWolverineStorageConfigurationException: There must be exactly one message store
    //   tagged as the 'main' store ... Found multiples: ...
    //
    // Ancillary says "this is my transport, not my node store", leaving your Marten
    // integration as Main.
    opts.UsePostgresqlPersistenceAndTransport(
            connectionString,
            transportSchema: "myapp_queues",
            role: MessageStoreRole.Ancillary)
        .AutoProvision();

    opts.AddCritterWatchEmbedded(postgresSource, schemaName: "critterwatch");
});

role: MessageStoreRole.Ancillary is required

Leave it out and your application fails to start:

InvalidWolverineStorageConfigurationException: There must be exactly one message store tagged as
the 'main' store, you may need to mark all but one message store as 'ancillary'. Found multiples: ...

A database-backed transport otherwise registers itself as a second Main message store, which collides with your own IntegrateWithWolverine() store. Ancillary tells Wolverine this is a transport, not a node store.

The console's own control channel stays in-process

With embedded mode the console's communicationUri is local://critterwatch-control whether or not you use database queues. Postgres queues are your transport; CritterWatch self-monitors in-process and does not put its telemetry on them.

Your host must not handle CritterWatch's message types ​

If your application declares a handler for one of CritterWatch's own messages — ServiceUpdates, for instance — the host refuses to start, naming the conflict.

That refusal is deliberate. Under Wolverine's default MultipleHandlerBehavior, a direct handler wins over CritterWatch's BatchMessagesOf<ServiceUpdates>() batch handler and silently shadows it. The shadowed handler is the telemetry-ingest batching path, so the cost is not a wrong answer but an ingest collapse — roughly 15 messages/minute against 110+/second inbound — with nothing logged. A startup failure you can read is strictly better, so CritterWatch promotes the warning to an error.

You are unlikely to hit this by accident: it requires handling a message type that belongs to the monitoring tool.

Supported configurations ​

Your hostStoreSupported
Single instanceMarten (PostgreSQL)✅
Single instancePolecat (SQL Server)✅
Single instanceFisher (SQLite)✅ — no database service needed at all
ClusteredMarten / Polecat⏭️ planned; needs a transport (see below)
ClusteredFisher (SQLite)⛔ not supported

Fisher/SQLite is single-instance only ​

This is a hard limit, not a gap we intend to close. SQLite has no queue transport for Wolverine, so a second node's telemetry has nothing to travel over, and Fisher's async daemon runs in Solo mode by design. A Fisher-backed embedded console on a clustered host refuses to start rather than run without the guarantee it claims — you get an error naming the limitation, not a console that quietly loses data.

If you want an embedded console on a clustered host, use Marten or Polecat.

Clustering needs a transport — but a database queue is enough ​

When embedded clustering arrives, it will need a real transport between nodes: if only one node ingests telemetry, the other nodes' telemetry has to reach it, and an in-memory queue only reaches its own process.

That transport can be a database queue. Wolverine ships a partitioned database-queue topology for both PostgreSQL and SQL Server, where each partition slot is its own queue table. So clustered embedded does not imply RabbitMQ or Azure Service Bus — the database you are already running is enough.

How your handlers stay yours ​

CritterWatch's own handlers and HTTP endpoints are pre-generated against its store and registered directly — your application's handler discovery and code generation never see them. Your application's handlers are left completely alone: they keep the store, session and transaction they already had, and nothing about their generated code changes.

There are no attributes to add anywhere — not on your code, and not on CritterWatch's.

Your error handling stays yours ​

The console's retry ladders (retry with cooldown, then discard — right for telemetry, which is re-sent every few seconds) are attached to the console's own handlers only. Your handlers keep whatever error handling you configured, and with none configured, Wolverine's default: a failed message goes to the dead-letter queue.

Fixed in 1.1.0-rc.1

Through 1.1.0-beta.8 the console registered those ladders host-wide, so in an embedding application every handler retried a failing message for more than six seconds and then discarded it instead of dead-lettering it. If you embedded an earlier beta, upgrade.

The console also leaves your endpoints' serializers alone (its wire format applies only to a standalone console's telemetry links), and it registers no wire tap of its own in your host.

The same holds for HTTP JSON. The console's API speaks its own format (camelCase, enums as camelCase strings) whatever your application configures on ASP.NET Core's JsonOptions, and it does not touch those options, so your Minimal API and Wolverine.HTTP endpoints keep your naming policy and enum format. Through 1.1.0-beta.8 it did both: with mapWolverineEndpoints: false (or a PascalCase JsonOptions) the console's API answered in PascalCase and the About page read undefined, and embedding switched your own endpoints' enums to camelCase strings.

What the console reports as your application ​

The console shares your process, so everything it reads off the runtime would include itself if it did not filter. It leaves its own parts out of what it shows about your application: its projections, event types, endpoints and slices are not on your capability card or in your Event Model, its message traffic is not in your metrics, its background agents are not in your agent list or timeline, and its own store is not offered in the Event Store Explorer, the SQL view or the Document Explorer. Dead-letter and scheduled-message operations do not fan out to the console's own database. These were fixed across 1.1.0-rc.1 and 1.1.0-rc.2; on an earlier build the SQL view refused every statement as ambiguous between two event stores.

What the console does add to your host ​

Embedding is not free of side effects. These are the host-wide registrations an embedded console still makes, so nothing about them is a surprise:

  • Health checks. critterwatch-system and critterwatch-liveness are added to your health-check registry. A health endpoint you map with no predicate reports them too.
  • SignalR. The Wolverine SignalR transport is registered, and HubOptions.ClientTimeoutInterval defaults to 120 seconds. That is the global HubOptions, so it applies to your own hubs unless you set it yourself.
  • Message grouping, for the console's own messages only. CritterWatch's messages carry a group id taken from their ServiceName (else Id) property, so the console processes one service's telemetry in order. The rule matches only CritterWatch's own message types, so your messages keep whatever grouping your own rules give them, wherever those rules are declared. Through 1.1.0-beta.8 it was a host-wide ByPropertyNamed("ServiceName", "Id") that took precedence over your rules for any message with an Id property.
  • Tracked-session ignore rules. The console's own messages never stop flowing, and a Wolverine tracked session (InvokeMessageAndWaitAsync, TrackActivity(), every Bobcat act) waits for activity to go quiet. So the console registers host-level ignore rules on WolverineOptions.Tracking for its own message types and WolverineHeartbeat, and your tests' sessions complete without knowing the console is there. A session that wants to see that traffic opts back in with TrackActivity().IncludeHostIgnoredTraffic().
  • Monitoring switches, again as for any monitored application: message causation tracking, event append tracking, durability metrics and heartbeats are turned on, and the metrics mode becomes Hybrid unless you chose one.
  • CritterWatch's own local queues ignore your durability policies. A host-wide opts.Policies.UseDurableLocalQueues() or AllLocalQueues(x => x.UseDurableInbox()), and even a per-queue UseDurableInbox(), does not make the console's queues durable. CritterWatch's own configuration wins, because its telemetry is self-superseding and a durable inbox would cost a write per message. Your own queues keep exactly what you gave them. The exceptions are the few operator-command queues that are durable by design, such as a deferred projection rebuild, which lands in your application's store, so the Scheduled Messages page can show and cancel it. Through 1.1.0-beta.8 the console's relay-command queues still took a host-wide durability policy. To scope a policy of your own, resolve Wolverine.CritterWatch.EventModeling.EmbeddedConsoleEndpoints from the container and skip any endpoint whose Owns(uri) is true.

Limitations, stated plainly ​

  • Self-monitoring only. An embedded console watches the host it lives in. It does not ingest telemetry from other services.
  • Single instance for the 1.1 release, on every store.
  • Stored documents are readable by anyone who can reach the mounted path, unless you wire permissions. Permissions are opt-in on an embedded console just as on a standalone one. The Document Explorer returns whole business documents, so if the mounted path is reachable by people who should not read them, turn on permissions (documents.read gates document reads), put the mount behind your own authorization with configureEndpoints, or set EnableDocumentExplorer = false through ConfigureMonitoring (see Putting the console behind your authorization).
  • Your host owns the runtime. The console shares your application's Wolverine runtime, node and listeners, though it keeps its own message store (see above). If you need the console isolated from your application's failures, run it standalone.

See also ​