Introspection
Telemetry answers what your application is doing. Introspection answers what it is: the route table, the merged configuration, the container bindings, the plugins, the files the scanner found. It is low-volume, request/response shaped data that only changes when your application does, which is why it is not modelled as an OpenTelemetry signal.
Everything goes through one registry, so a consumer asks a single API instead of reaching into Router, Container and the config on its own.
Reading a section
import { Inject } from '@vercube/di';
import { IntrospectionRegistry } from '@vercube/core';
class RouteAudit {
@Inject(IntrospectionRegistry)
private gIntrospection!: IntrospectionRegistry;
public async unvalidatedRoutes() {
const section = await this.gIntrospection.describe('routes');
return section!.data.filter((route) =>
route.args.some((arg) => arg.type === 'body' && !arg.validated),
);
}
}
describe(id) returns { id, title, revision, data }. list() gives every registered section and its current revision without describing any of them, and describeAll() describes them all at once.
Built-in sections
| Section | Contributed by | Contents |
|---|---|---|
routes | @vercube/core | One entry per registration, with controller, handler, arguments and middleware chain |
config | @vercube/core | The merged configuration and the runtime slice, flattened to dotted paths, with credentials withheld |
container | @vercube/di | Bindings, dependency edges, detected cycles and what has been instantiated |
plugins | @vercube/core | Registered plugins and application-wide middlewares |
discovery | @vercube/vite | What the build-time scanner found, keyed by class name so runtime bindings can be traced back to a file |
openapi | @vercube/schema | The generated OpenAPI document |
storage | @vercube/devtools | Mounted storages and the cache configuration |
@Get registers both GET and HEAD, so the routes section reports both. Folding the pair into one row is a presentation choice and is left to whoever renders the list.
Revisions
Every provider reports a revision that changes whenever its data would come out different. The registry memoizes a described section against the revision it was built from, which is what stops a dashboard showing four panels from rebuilding the dependency graph four times.
Consumers can use it too. Devtools turns it into an ETag, so re-opening a panel gets a 304 rather than a fresh graph.
Contributing a section
import { Inject } from '@vercube/di';
import { BasePlugin, IntrospectionRegistry } from '@vercube/core';
import type { App } from '@vercube/core';
export class QueuePlugin extends BasePlugin {
public override name = 'QueuePlugin';
public override use(app: App): void {
const manager = app.container.get(QueueManager);
app.container.getOptional(IntrospectionRegistry)?.register({
id: 'queues',
title: 'Queues',
revision: () => manager.revision,
describe: () => manager.describe(),
});
}
}
Two rules are worth respecting. describe() must be side-effect free: an inspector that constructs the thing it inspects is not an inspector. And revision() must be cheap, because it is called every time anything asks what has changed.
Call registry.touch(id) after your data changes to notify live consumers. Without it, the cache still notices on the next read; touch is what turns that into a push.
From the command line
$ vercube inspect
$ vercube inspect --section routes
$ vercube inspect --section routes,container
inspect builds your application and runs its own entry file, stopping it right before it would bind a port, then prints every section as JSON. That is the only way to see the routes and bindings your setup actually produces, which makes it usable as a CI check:
$ vercube inspect --section routes | jq '[.routes.data[] | select(.args[]?.validated == false)] | length'