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
| Class | Role |
|---|---|
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:
| Constant | Value | When 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 telemetry —
log.db_querieswrites toquery.log;telemetry.db_queriesfillsTelemetryCollector. 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.