Skip to content

Runtime hooks

Runtime hooks

Beyond providers (publish metrics) and exporters (ship signals), a set of resolver hooks lets an app — or a package building on top, like a CMS integration — shape what the built-in instrumentation records. All of them are guarded: a throwing resolver is reported and ignored, never breaking the request.

Request span naming — nameRequestsUsing()

Essential behind catch-all routes (Statamic, wildcard APIs), where the route pattern names every request identically:

Telemetry::nameRequestsUsing(function ($request, $response) {
    $entry = $request->attributes->get('resolved.entry');

    return $entry ? 'GET entry:'.$entry->collection : null; // null = default name
});

Keep names bounded — collections and types, never ids or slugs. Precedence: an explicit updateName() on the span during the request always wins; then this resolver; then the default METHOD /route/{pattern}. http.route keeps the raw route pattern regardless.

Root-span enrichment — enrichRequestsUsing()

Extra attributes on the request root span at terminate, with the final response in hand (status-dependent enrichment works):

Telemetry::enrichRequestsUsing(fn ($request, $response) => [
    'app.static_cache' => $response->headers->get('X-Cache', 'miss'),
]);

Runs before the tail-detail decision and the redaction engine. For metric labels use labelRequestsUsing() instead — attributes are per-span (unbounded ok), labels multiply cardinality (bounded only).

Cache key classification — classifyCacheKeysUsing()

Cache-heavy subsystems (a CMS content cache, an ORM cache) produce thousands of raw keys. Classify them into bounded groups — or drop them:

Telemetry::classifyCacheKeysUsing(function (string $store, string $key) {
    if (str_starts_with($key, 'stache::indexes::')) {
        return 'stache.index';
    }

    return str_starts_with($key, 'internal:') ? null : 'app'; // null = drop
});

With a classifier registered, kept operations carry the group as a key_group counter label and a cache.key.group span attribute (the raw key stays on the span). Whole stores can be excluded with instrument.cache_ignore_stores.

The full hook surface

Hook Shapes Signature
nameRequestsUsing() root span name fn ($request, $response): ?string
enrichRequestsUsing() root span attributes fn ($request, $response): array
labelRequestsUsing() request metric labels (bounded!) fn ($request): array
resolveUserUsing() user attribution fn ($user, ?string $guard): array
classifyCacheKeysUsing() cache grouping/dropping fn (string $store, string $key): ?string
redactUsing() last-pass redaction fn (string $key, string $value): ?string
handleExceptionsUsing() internal-failure reporting fn (Throwable $e): void
Telemetry::context() ambient dimensions on all signals
Tracer::recordSpan() / bumpStat() / rootSpan() custom spans, backdated spans, root tallies
Telemetry::contributes() conditional registration when telemetry exists

A package integration typically combines these: a user resolver, a context() listener for its ambient dimensions (site, tenant), a request namer for its routing model, a cache classifier for its cache traffic, and a TelemetryProvider for its gauges.