Telemetry
Prism emits Laravel events for the generation lifecycle, including steps, tool calls, completion and failure. Telemetry is disabled by default.
Use it to trace and measure generations — latency, token usage, cost, finish reasons, tool calls — wired into your own logging and metrics, or exported to an OpenTelemetry backend such as Arize Phoenix with the companion bridge.
Enabling telemetry#
Telemetry is disabled by default. Turn it on with an environment variable:
PRISM_TELEMETRY_ENABLED=true
The full configuration block in config/prism.php:
'telemetry' => [ // Master switch. When false, telemetry is a complete no-op: no contexts // are minted and no events are dispatched. 'enabled' => env('PRISM_TELEMETRY_ENABLED', false), // Include prompts, completions, and tool arguments in payloads. Off by // default — this data can contain PII. Only enable where the sink is trusted. 'capture_content' => env('PRISM_TELEMETRY_CAPTURE_CONTENT', false), // Bounds so telemetry can never turn a streaming response into unbounded memory. 'content_max_length' => (int) env('PRISM_TELEMETRY_CONTENT_MAX_LENGTH', 65_536), 'content_max_items' => (int) env('PRISM_TELEMETRY_CONTENT_MAX_ITEMS', 256), ],
The events#
All telemetry events live in the Prism\Prism\Events\Telemetry namespace and
are dispatched through Laravel's event system, so you consume them with ordinary
listeners.
| Event | Dispatched when | Key payload (beyond $context) |
|---|---|---|
GenerationStarted |
a generation begins | $request, $tools |
StepCompleted |
each step finishes | $finishReason, $usage, $rateLimits, $step |
ToolInvoked |
a tool call resolves | $toolName, $toolCallId, $durationMs, $toolCall, $toolResult |
GenerationCompleted |
the generation finishes | $durationMs, $finishReason, $usage, $rateLimits, $response |
GenerationFailed |
the generation throws | $durationMs, $exception |
Every event carries a TelemetryContext (see below)
that ties the whole lifecycle together.
Listening#
Subscribe like any Laravel event:
use Illuminate\Support\Facades\Event; use Prism\Prism\Events\Telemetry\GenerationCompleted; Event::listen(function (GenerationCompleted $event): void { logger()->info('prism.generation', [ 'trace_id' => $event->context->traceId, 'provider' => $event->context->provider, 'model' => $event->context->model, 'duration_ms' => $event->durationMs, 'finish_reason' => $event->finishReason?->value, 'prompt_tokens' => $event->usage?->promptTokens, 'completion_tokens' => $event->usage?->completionTokens, 'rate_limits' => array_map(fn ($limit) => $limit->toArray(), $event->rateLimits), ]); });
For anything more involved, register a listener class in your application's event service provider.
The telemetry context#
Prism\Prism\Telemetry\TelemetryContext is the correlation object shared by
every event of a single generation:
| Property | Description |
|---|---|
traceId |
stable id for the whole generation — join every event on this |
operation |
a TelemetryOperation describing the kind of call |
provider / model |
the provider key and model id |
stepIndex / toolIndex |
ordinals nesting steps and tool calls under the generation |
userId / sessionId |
your optional metadata (see below) |
startedAt |
monotonic start; elapsedMs() returns elapsed time |
Attaching user & session metadata#
Correlate telemetry with your own users and conversations using the
withTelemetryMetadata() builder method — the values flow onto every event's
context:
use Prism\Prism\Facades\Prism; use Prism\Prism\Enums\Provider; $response = Prism::text() ->using(Provider::Anthropic, 'claude-sonnet-5-5') ->withTelemetryMetadata(userId: (string) $user->id, sessionId: $conversation->id) ->withPrompt('Summarise this thread') ->asText();
Every telemetry event for that call now carries $context->userId and
$context->sessionId, so you can group traces per user or per conversation in
your backend.
Capturing prompt & completion content#
By default telemetry records structure and metrics only — timings, token usage, finish reasons, model, and the provider's rate-limit buckets — never the prompt or completion text, because that content can contain PII.
capture_content controls prompt and response content. Usage and rate-limit
metadata remain available when provided by the API.
When telemetry is enabled, GenerationStarted::$tools also carries each
advertised tool's name and a SHA-256 digest of its declaration (name, description
and parameters), even when capture_content is off. Full descriptions and
parameter schemas remain behind the content gate. These fingerprints help
diagnose cache misses caused by changes to the tool list in a provider's prompt
prefix.
Use stable, application-defined tool names. If you derive a tool name from user data, such as a user-owned document's title, that name is exposed to telemetry listeners on every generation that advertises the tool, even with content capture disabled. The digest is a fingerprint, not a copy of the declaration; it still allows comparison with known or guessed declarations.
Opt in with capture_content, and only where the telemetry sink is trusted:
PRISM_TELEMETRY_CAPTURE_CONTENT=true
Configure capture limits to bound the amount of retained content:
content_max_length(default65_536) caps per-item text length.content_max_items(default256) caps how many message items are captured.
Attachments#
Captured messages can hold images, documents, audio or video. Their bytes are not
captured by default. A media part keeps its kind, mime type, file id and filename,
and its base64 is replaced by omitted_bytes, the size of what was left out:
{"kind": "image", "url": null, "base64": null, "mime_type": "image/png", "file_id": null, "filename": null, "omitted_bytes": 48213}
To capture the bytes too, where your telemetry sink is trusted with your users' files:
PRISM_TELEMETRY_CAPTURE_MEDIA=true
Only media Prism serialises is recognised. Bytes a user pastes into a prompt as text
are text, and capture_content alone decides whether they are captured.
Exporting to OpenTelemetry / Arize Phoenix#
Because the events above are provider-neutral, any exporter can consume them. The
companion package
particle-academy/prism-opentelemetry
subscribes to them and emits standard OpenTelemetry spans — a root span per
generation, child spans per step and per tool call, with token, cost, model, and
finish-reason attributes, in both the GenAI and OpenInference conventions — over
OTLP to Arize Phoenix, Jaeger, Tempo, or any OTLP
backend.
composer require particle-academy/prism-opentelemetry
With telemetry enabled and the bridge installed, generations are exported as nested traces.