Telemetry

Concept Core telemetry collects metrics and marks for the request lifecycle (bootstrap, HTTP, templates, SQL) for debugging and profiling. It is not a domain event bus: the core does not provide EventDispatcherInterface for business events like UserRegistered — add that logic in your own service provider (PSR-14, Symfony, etc.).

See also: Service providers → TelemetryTrait, request lifecycle, configuration.

Enabling

Key telemetry.enabled (ConfigKey::TELEMETRY_ENABLED):

// config/telemetry.php
return [
    'telemetry' => [
        'enabled' => false,
        'db_queries' => false,
        'logs' => false,
    ],
];

// config/dev/telemetry.php — override for APP_ENV=dev
return [
    'telemetry' => [
        'enabled' => true,
        'db_queries' => true,
        'logs' => false,
    ],
];

Default in skeleton — false; in dev override — true. While the flag is off, TelemetryTrait::telemetry() returns null and the core does not record events (except where the collector is passed explicitly — see below).

Telemetry components

ClassRole
TelemetryServiceProvider Registers shared TelemetryCollector in the container
TelemetryCollector Event storage: start(), finish(), record(), mark(), items()
TelemetryTrait Access to the collector respecting telemetry.enabled (in providers, RouteStrategy)
TelemetryEvent Event name constants (string identifiers)
TelemetryItem Single record: context, started_at, finished_at, duration
TelemetryLogHandler Monolog handler: mirrors log records to LOG_RECORDED when telemetry.logs is on

TelemetryServiceProvider is early in bootstrap/providers/app.php (after ConfigServiceProvider and LocaleServiceProvider), so other providers can write telemetry during register().

Built-in records

Identifiers in Concept\Core\Services\Telemetry\TelemetryEvent:

ConstantValueWhen recorded
FRAMEWORK_SERVICE_AWAKENING framework.service_awakening First creation of a service in a provider (mark with class name)
FRAMEWORK_COMPONENT_REGISTERED framework.component_registered ComponentsServiceProvider::boot() — each active component
FRAMEWORK_ROUTES_REGISTERED framework.routes_registered HttpServiceProvider after loading route files from routes.list
HTTP_ROUTE_INTERCEPTOR_EXECUTED http.route_interceptor_executed Each route interceptor in RouteStrategy::runInterceptors()
HTTP_ROUTE_CALLABLE_INVOKED http.route_callable_invoked Handler invocation in RouteStrategy (start/finish)
HTTP_REQUEST_HANDLED http.request_handled End of App::run() — method, path, memory, total duration
TPL_RENDERED tpl.rendered Twig render in TwigView::render() (template name, duration)
DB_QUERY_EXECUTED db.query_executed SQL listener in DatabaseServiceProvider (if telemetry.db_queries)
LOG_RECORDED log.recorded Monolog record via TelemetryLogHandler (if telemetry.logs)
HTTP_FORM_REQUEST_VALIDATED http.form_request_validated Reserved in the API; the core does not emit this record yet

Collector API

mark() — instant mark

For point-in-time marks without duration measurement (service awakening):

$this->telemetry()?->mark(
    TelemetryEvent::FRAMEWORK_SERVICE_AWAKENING,
    SessionInterface::class
);

start() / finish() — span

$id = $this->telemetry()?->start(TelemetryEvent::HTTP_ROUTE_CALLABLE_INVOKED, [
    'route' => $route,
    'handler' => 'UserController::index',
]);
try {
    // ...
} finally {
    $this->telemetry()?->finish(TelemetryEvent::HTTP_ROUTE_CALLABLE_INVOKED, (string) $id);
}

record() — instant span

Combines start() and finish() in one call (used for SQL, routes, HTTP summary):

$this->telemetry()?->record(
    TelemetryEvent::DB_QUERY_EXECUTED,
    [TelemetryKey::SQL => $query->sql],
    $query->time / 1000
);

Reading data

/** @var TelemetryCollector $telemetry */
$telemetry = $container->get(TelemetryCollector::class);

$services = $telemetry->items(TelemetryEvent::FRAMEWORK_SERVICE_AWAKENING);
$all = $telemetry->toArray();

The collector is a singleton per request. After the HTTP request completes, data is not persisted across requests (unless the application exports it itself, e.g. to DebugBar).

TelemetryTrait in providers

Core providers (HttpServiceProvider, SessionServiceProvider, DatabaseServiceProvider, etc.) use TelemetryTrait:

private function telemetry(): ?TelemetryCollector
{
    // 1. Is TelemetryCollector in the container?
    // 2. telemetry.enabled === true?
    // 3. Otherwise — null (recording is skipped)
}

In your own bootable provider you can use the same trait if the class has getContainer() (as in AbstractServiceProvider).

Example: DebugBar

The DebugBar component reads TelemetryCollector and shows widgets for “Services”, route handler, and the last rendered template:

// ServicesDataCollector
$items = $this->telemetryCollector->items(TelemetryEvent::FRAMEWORK_SERVICE_AWAKENING);
foreach ($items as $item) {
    $className = $item->getContext()['name'] ?? 'Unknown';
    // ...
}

Enable telemetry.enabled and the DebugBarComponent in config/dev/components.php.

Custom records

In an application service or middleware:

public function __construct(
    private readonly TelemetryCollector $telemetry,
    private readonly ConfigInterface $config,
) {}

public function heavyOperation(): void
{
    if (!$this->config->getBool('telemetry.enabled')) {
        return;
    }

    $id = $this->telemetry->start('app.heavy_operation', ['step' => 'import']);
    try {
        // ...
    } finally {
        $this->telemetry->finish('app.heavy_operation', $id);
    }
}

Name prefix (app.) is an application convention; the core does not validate arbitrary strings. For constants in your code, create a class like AppTelemetryEvent.

What is not in the core

  • Domain events — listener/subscriber registration for business events; wire PSR-14 or Symfony EventDispatcher in your own provider
  • EventServiceProvider — no such class in concept-core (see request lifecycle)
  • SQL logging vs telemetrylog.db_queries writes to query.log; telemetry.db_queries fills TelemetryCollector. The flags are independent.

Telemetry is for debugging and profiling, not business logic. Do not rely on it in production code as a mechanism for integration between modules.