Introspection

Ask a running Vercube application what it looks like

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

SectionContributed byContents
routes@vercube/coreOne entry per registration, with controller, handler, arguments and middleware chain
config@vercube/coreThe merged configuration and the runtime slice, flattened to dotted paths, with credentials withheld
container@vercube/diBindings, dependency edges, detected cycles and what has been instantiated
plugins@vercube/coreRegistered plugins and application-wide middlewares
discovery@vercube/viteWhat the build-time scanner found, keyed by class name so runtime bindings can be traced back to a file
openapi@vercube/schemaThe generated OpenAPI document
storage@vercube/devtoolsMounted 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'
Previous

Overview

Flexible authentication system for Vercube applications

Next