Skip to content
Improve this page

Inspect over HTTP

Goal: look inside a running service's caches without attaching a debugger: sizes, load times, and ad-hoc queries by index.

Prerequisite: the Prague.Api package and an ASP.NET Core host with AddKafkaCaches. The code is in InspectOverHttp.cs; the mounting blocks are compiled-only, the requests run against the real endpoints in-process over a hand-built registry.

1. Mount

public static void MountOpen(WebApplication app) {
    app.MapPragueEndpoints();      // GET /prague · GET /prague/{CacheName} · GET /prague/{CacheName}/{id} (parsable keys only)
}

MapPragueEndpoints resolves IDataCacheRegistry, KafkaCachesStatistics and IOptions<KafkaCachesGlobalOptions>; AddKafkaCaches registers all three. For Prague.Api.UI, add builder.AddPragueUI() and app.MapPragueUI() next to it.

2. Statistics

var statistics = await _client.GetStringAsync("/prague");   // consumers → caches → sizes, load times, index sizes

The JSON is Consumers → one entry per AddKafkaCaches section → Caches → per-cache size, load time, messages received and per-index sizes. Property names are PascalCase (the serializer is source-generated without a naming policy).

3. Query a cache

GET /prague/{CacheTypeName} runs the generated query-string parser against the cache: indexed fields become lanes, the rest become Where filters, and the result is the first 100 rows unless you page.

var byCustomer = await _client.GetStringAsync("/prague/OrderCache?customerId=42");          // index lane
var paidAndBig = await _client.GetStringAsync("/prague/OrderCache?status=Paid&total.gte=100");   // lane + residual filter
var window = await _client.GetStringAsync("/prague/OrderCache?createdAtUnixMs=1000..2000&size=1&from=1");   // range + page
var firstHundred = await _client.GetStringAsync("/prague/OrderCache");                      // no query: first 100 rows
// Typed into a browser, `total>=100` works too; HttpClient percent-encodes `>` and `[`, which the parser does not decode.
Syntax Meaning
field=value equality (field=1,2,3 or field=[1,2,3] is an IN)
field>=10, field.gte=10 also >, <, <= and .gt, .lt, .lte
field=10..100, field=[10..100) range; [ ] inclusive, ( ) exclusive
from=20&size=10 offset and page size

Field names are matched case-insensitively. An unknown field is ignored, not rejected. sort= is parsed but does not reorder the result.

4. By id

GET /prague/{CacheTypeName}/{id} is mapped only for caches whose generated IsKeyParsable is true. For the sample caches, OrderCache (string key) and CustomerCache (int key) both report false, so the route is a 404; query the key field instead: /prague/OrderCache?orderId=ORD-001.

5. Keep it internal

MapPragueEndpoints maps onto any IEndpointRouteBuilder, so a route group gives it a prefix, an authorization policy, or a host restriction.

public static void MountSecured(WebApplication app) {
    // MapPragueEndpoints maps onto any IEndpointRouteBuilder, so a route group gives it a prefix and a policy.
    var ops = app.MapGroup("/internal").RequireAuthorization("ops");   // → /internal/prague/...
    ops.MapPragueEndpoints();
}

public static void MountOnLoopbackOnly(IEndpointRouteBuilder app) {
    app.MapGroup("").RequireHost("localhost", "127.0.0.1").MapPragueEndpoints();   // or bind a separate management port
}

Pitfalls

  • Encoded operators. HttpClient percent-encodes > and [ in a URI string and the parser does not decode, so total>=100 becomes an unknown field and is ignored. From code use the suffix form total.gte=100 and the bare range 1000..2000; typed into a browser both forms work.
  • Treating the collection endpoint as an API. It allocates and serializes the cache's own instances; it is for inspection. Put real traffic behind your own endpoints (Page an API).
  • Mounting it on the public port. It reads every cache. Use step 5, or bind it to a management port.