cboxdk/laravel-telemetry
Collector-free telemetry for Laravel: Prometheus metrics aggregated in Redis or APCu, plus OTLP traces, logs and events over plain HTTP JSON. Works under FPM, auto-instruments requests, queue jobs, DB queries and commands.
KEYS/SCAN scans. That directly unblocks product-facing metrics — signups, orders placed, checkout durations, queue depth — which most PHP teams currently approximate with log parsing or give up on.instrument.ai_pricing.models, keys like anthropic/claude-sonnet-*, with input/output/cache-read/cache-write rates); each priced model and embeddings call then emits gen_ai.usage.cost and increments a gen_ai.client.cost counter carrying the same labels as the token metric, with the currency as its unit. Cache reads and writes are billed at their own rates, and an agent run carries the sum of its calls when every call was priced. For any product with per-seat or per-tenant COGS, this turns "we think AI features are profitable" into a number you can join to plan, region, and tenant. It is the single highest-leverage roadmap input in the package — and it unblocks concrete pricing/packaging decisions (AI included in plan vs. metered, gross margin per tenant) rather than just adding another dashboard.gen_ai.client.inference.operation.details record (one per model call, with tokens, cost, user, and context dimensions, never content) is emitted as a log record, which means it is not sampled — per-user and per-tenant sums are exact rather than extrapolated from a trace sample. That is the difference between telemetry you can look at and telemetry you could bill or quota against. The enabling decision this forces, deliberately, is an explicit one: we must decide whether attaching user.id to telemetry is acceptable under our privacy posture, and budget log-pipeline storage for the per-call records.traces.details.keep_when (keep a whole trace's detail spans when any span matches, e.g. ['http.route' => 'checkout*'] or ['gen_ai.operation.name' => 'invoke_agent']) and traces.details.keep_spans (keep matching detail spans in every trace, e.g. writes via ['db.query.text' => ['insert *', 'update *']]). The product decision this enables is a defensible default: sample aggressively for cost, but keep full fidelity on the revenue path, on agent invocations, and on all writes — declared in config and reviewable, instead of "we turned sampling down and lost the checkout traces."laravel.routing_ms, middleware_ms, handler_ms, terminate_ms), query tallies (db.query.time_ms, slow-query counts, model hydrations), and automatic N+1 vs. duplicate-query classification with the offending app file and line. That converts "the app feels slow" into a ranked backlog of specific loops and Blade views. Since v3.8.1 the same applies to async work: a job attempt that throws and is released for retry now carries the exception (event, error.type, message as span status) instead of an opaque released for retry, so a job retried twenty times reads as one diagnosable problem rather than twenty red spans and no evidence.Telemetry::fake() with assertions like assertNoNPlusOne() and assertQueryCount('<=', 10) lets performance become a CI assertion rather than a post-incident discovery. This supports a standing decision to treat query-count budgets as a merge gate on hot endpoints.telemetry:watch --route=... --minutes=30, per-user, per-tenant context) let support and engineering raise detail on one slice for a bounded window. As a product decision, this means "we can investigate a customer complaint without raising cost or PII exposure globally" — a much easier ask than flipping a global sample rate.X-Trace-Id), full W3C traceparent propagation across HTTP and queue boundaries, and optional Server-Timing headers give you the primitives to attach trace ids to support tickets and define latency SLOs per route. Queue-boundary propagation plus v3.8.1's per-attempt exception capture means an "order stuck in processing" ticket resolves to a specific attempt and failure reason, not just a correlation id.Adopt when:
laravel-telemetry-ui and its MCP tooling are optional and add another dependency.user.id appearing on AI spans and per-call usage records. If identity-attached telemetry is off the table for you, the per-user/per-tenant metering story degrades to plan/tenant context labels only.Look elsewhere (or pilot narrowly) when:
Pilot exit criteria I would set before committing: overhead measured with the package's own benchmark group on a representative endpoint (target: low single-digit % of request time with tracing off, acceptable bounded cost with sampling on); confirmed aggregation correctness across ≥2 FPM workers; a working trace-id → support-ticket workflow; one model priced and reconciled to within an agreed tolerance of the provider invoice for a single tenant over a full billing period; at least one keep-rule in production (e.g. checkout* detail retention) with a stated, reviewed storage cost; one real failed-and-retried job traced end-to-end to an exception message on the attempt's span; and one cost or capacity decision actually made from the data within a quarter.
To executives — the decision and the money: This is a candidate replacement for per-host APM licensing in our Laravel services, using an MIT-licensed library instead of a paid agent. It produces industry-standard Prometheus metrics and OpenTelemetry traces, so nothing we build on it is trapped: if we later buy a vendor, we change a configuration target, not our code. The concrete business returns we would target are (1) lower observability spend per host, (2) faster root-cause on "the app is slow" and "the job is stuck" tickets because the trace names the file, line, and the exception that caused the retry, and (3) defensible unit economics — per-tenant and per-user AI cost computed inside the app from a price table we control, with exact (unsampled) per-user sums available when we opt in, plus queue-driven capacity numbers instead of estimates. The middle item is not hypothetical: the latest patch (v3.8.1) was purely about making failed async work explain itself, which is where most of our support cost hides and where we currently get nothing from log grep. The honest risks are maturity (small, single-maintainer, effectively no external production footprint) and ownership: no price list ships, so cost accuracy is a config we must curate, and identity is attached to AI telemetry, which needs a privacy sign-off. Recommendation: time-boxed pilot on one internal or non-critical service with a named owner, not a fleet-wide mandate.
To engineering — why it is not another fragile sidecar: It solves the specific thing that has blocked PHP metrics for us: shared-nothing FPM. Metric state lives in a store every worker writes to (Redis or APCu behind a contract), with explicit indexes — no KEYS/SCAN, no full-keyspace iteration, so it will not take out Redis under load. There is no C extension, no protobuf, no collector required; OTLP goes out over raw curl. Telemetry never throws into the application — all recording and export paths are guarded — which is the property that makes this safe to run in production. It is zero cost when disabled: no listeners, no-op instruments, no providers booted. v3.8.1 is exactly the kind of change you want from a dependency at this stage: no new config, no API surface change, no upgrade work — it fixes error attribution on queue spans so a released-for-retry attempt carries the exception event, error.type, and message as span status (sourced from JobExceptionOccurred on every supported Laravel version, JobReleasedAfterException on 13.31+), with terminal failures still recording one exception event per attempt rather than two. That is a real fidelity bug in the failure path fixed promptly, and it tells us the maintainer is watching the boring parts. Naming follows OTel semantic conventions with Prometheus names derived, never stored. Code quality signals are above what we normally accept from a small dependency: strict types, Larastan level 9, PHP 8.3, decision records for the hard calls, and an overhead benchmark we can run ourselves before signing off. Testing is first-class — Telemetry::fake() lets us assert no N+1 and a query-count ceiling on hot endpoints. Two things to size before enabling: the per-call usage log records are unsampled by design (that is what makes sums exact, and it is also a log-volume line item), and keep_when/keep_spans trade storage for fidelity, so they need a stated budget, not a default.
To both — the risk sentence, said out loud: Low adoption is the risk, and the mitigation is structural rather than contractual. MIT license means we can fork; the OTLP/Prometheus boundary means the exit ramp costs a config change, not a rewrite; and the instrumentation we write is business-level (orders.created, invoice.overdue, plus our own model price table) so it survives any backend swap. The v3.8.1 fix reinforces rather than removes the maturity caveat: it shows responsive maintenance on a real defect, but a prompt patch from a single maintainer is not a support commitment. What we should not assume is vendor-grade support, security response times, API stability, or a maintained pricing catalog — that config is ours to keep correct. Fund the pilot accordingly and keep the decision reversible.
How can I help you explore Laravel packages today?