PHP
zharp/zharp sends a Laravel, Symfony or plain PHP app's logs and exceptions, and traces every request it handles.
This SDK is new, and its API may still change before 1.0. Requires PHP 8.2 or later, as the OpenTelemetry PHP SDK does.
It is a thin layer over the official OpenTelemetry PHP SDK: Zharp configures the exporter, resource, W3C trace context and baggage propagation and a log bridge, and creates the request span itself.
Install
composer require zharp/zharp
Composer may ask to allow the php-http/discovery and tbachert/spi plugins, which the OpenTelemetry SDK uses. Allow them.
Initialize
Laravel
Nothing to register: the service provider is discovered on its own. Add to .env:
ZHARP_KEY=zpk_… ZHARP_SERVICE=checkout-api ZHARP_ENVIRONMENT=production
Telemetry is sent once the response is out, and after every queue job. php artisan vendor:publish --tag=zharp-config publishes config/zharp.php.
Symfony
Set the same environment variables, then enable the bundle:
// config/bundles.php
return [
// …
Zharp\Symfony\ZharpBundle::class => ['all' => true],
];Plain PHP
Zharp\Zharp::init([
'key' => 'zpk_…',
'service' => 'checkout-api',
'environment' => 'production',
]);Logs
In Laravel, everything Laravel logs at info and above is sent with the trace, user and session; with Symfony's MonologBundle, every channel's logs are. Anywhere else, give Monolog Zharp's handler, or write to Zharp directly:
$logger->pushHandler(new Zharp\Monolog\ZharpHandler('info'));use Zharp\Zharp;
Zharp::info('Order placed', ['order_id' => 42]); // debug, info, warn, error, fatalLogs only
If you only want logs, turn tracing off. Logs, errors, and the user and session they belong to are still sent; no spans are recorded and no trace headers are added to outgoing requests. A request from a Zharp front end is still read, so its logs keep the caller's user, session and trace ID.
// Laravel and Symfony need no code: ZHARP_KEY, ZHARP_SERVICE and ZHARP_TRACING=false in .env
Zharp\Zharp::init([
'key' => 'zpk_…',
'service' => 'checkout-api',
'tracing' => false,
]);Or leave the code as it is and set ZHARP_TRACING=false in the environment.
The logs show on the Logs page under the service name, and on the app's Logs tab with the user and session of each line. The key comes from an app — see Get a key. To add tracing later, remove the option; nothing else changes.
Errors
Exceptions Laravel reports, and exceptions that escape a Symfony controller (4xx HttpExceptions aside), are sent as unhandled errors with the request's trace. In plain PHP, an uncaught exception or fatal error is reported. Report one you caught yourself:
try {
$payments->charge($order);
} catch (PaymentException $e) {
Zharp::captureError($e, ['order_id' => $order->id]); // reported as handled
}Readable stack traces
Nothing to upload: the stacks this SDK sends already name your functions, files and lines, and issues are grouped by them.
Tracing
Every HTTP request gets a span — named by its route in Laravel (GET /orders/{id}) and by the route name in Symfony — continuing the caller's trace. Zharp\RequestSpan::start(...) makes a request span for a framework without an integration.
Zharp::startSpan('render invoice', fn () => render($invoice));
Zharp::setTag('region', 'eu-west'); // on every span and log after
Zharp::flush(); // send nowDatabase and HTTP client spans
PHP needs the opentelemetry extension for automatic instrumentation. With it installed, add the instrumentations you want; they use the providers Zharp registers, so their spans join the request's trace:
composer require open-telemetry/opentelemetry-auto-pdo open-telemetry/opentelemetry-auto-guzzle
Without the extension, Laravel and Symfony requests are still traced by Zharp's middleware and subscriber — you just don't get a span per query or outgoing call.
User context
Requests from the Zharp browser and mobile SDKs carry the user and session in baggage, and the request's span and logs get them with no code. When the server knows better:
Zharp::setUser(id: $user->id, email: $user->email); // this request's user, when the server knows better
Options
| init() option | Environment | Default |
|---|---|---|
key | ZHARP_KEY | — (nothing is sent without one) |
service | ZHARP_SERVICE, OTEL_SERVICE_NAME | APP_NAME in Laravel, else unknown-service |
environment | ZHARP_ENVIRONMENT | APP_ENV in Laravel |
release | ZHARP_RELEASE | none |
endpoint | ZHARP_ENDPOINT | https://api.zharp.io/api/v1/otlp |
tracing | ZHARP_TRACING | true; false is logs only |
sample_rate | ZHARP_SAMPLE_RATE | 1.0 — share of traces kept; logs and errors are always sent |
| — | ZHARP_LOG_LEVEL | info; off sends no framework logs |
capture_uncaught | — | true (Laravel and Symfony use the framework's handler instead) |
| — | ZHARP_DISABLED | true starts nothing |
Troubleshooting
Nothing arrives
On the app's setup page (Applications → the app → settings → Setup instructions), the verify step shows the first event as soon as it lands, and Send a test event checks the key on its own. If the test event arrives but your app's don't, the SDK isn't starting or isn't reaching Zharp:
- Is
initcalled, and before the code you expect to see? Without a key, the SDK starts nothing and says so once. - Does the service name match? With an app key everything goes to that app whatever it says; with a workspace key,
servicedecides the app — see Keys. - Data is sent in batches every few seconds. A process that exits straight away should call
flushfirst. - Logs and errors arrive but no traces? Check that tracing isn't turned off — see Logs only.
- In Laravel, is the config cached? Run
php artisan config:clearafter changing.env.
401 or 403
Ingest answers 401 when the key is missing, mistyped, revoked or rolled, and 403 when the request comes from an origin or IP address the key doesn't allow. Neither is retried: the SDK warns once and drops what was refused, and it never throws into your code. Copy the key again from the app's settings.