boltZharp Docs
phpLogs & Applications

PHP & Laravel Logs

zharp/logs sends logs and exceptions from PHP apps — a Laravel log channel in two lines of .env, a Monolog handler for Symfony, or calls of its own in plain PHP. No dependencies; PHP 7.4 and up.

Install

composer require zharp/logs

API key

  • In the dashboard, open Integrations & API and choose Main API keys.
  • Click Create API key, give it a name, and tick Ingest only. That key can send data but has no access to the REST API, so it's safe in a server's environment — and it's available on every plan.
  • Click Create key and copy the key. It's shown only once.
Create API key, with Ingest only tickedThe new key, shown once with a Copy button

Laravel

Nothing to register — Laravel discovers the package. Add two lines to .env:

.env
ZHARP_API_KEY=your-ingest-only-key
LOG_STACK=single,zharp

That's all. Everything Laravel logs now goes to Zharp as well as your log file — including every exception Laravel reports, with its class and stack. Logs are filed under your APP_NAME and tagged with your APP_ENV, unless you set ZHARP_SERVICE and ZHARP_ENV. Check it works:

php artisan zharp:test
Sent. Open Logs in Zharp and pick the service "checkout-api".

On Laravel 10 and older, add zharp to the stack in config/logging.php instead of setting LOG_STACK:

// config/logging.php — Laravel 10 and older
'stack' => [
    'driver' => 'stack',
    'channels' => ['single', 'zharp'],
],

Then log as you always have:

use Illuminate\Support\Facades\Log;

Log::info('User signed in', ['user_id' => $user->id]);
Log::warning('Slow network detected', ['latency_ms' => 3200]);

try {
    $gateway->charge($order);
} catch (Throwable $e) {
    report($e);   // sent with its class and stack
}
  • Uncaught exceptions and fatal errors are Laravel's to handle: it reports them through the log, so they reach Zharp through the channel too.
  • Logs are sent once the response is out, and after every queued job and every Octane request, since those processes don't end in between.
  • ZHARP_LEVEL sets the lowest level the channel sends (debug by default). php artisan vendor:publish --tag=zharp-config gives you config/zharp.php to set it all in code.

Plain PHP

Call Zharp::init() once, as early as possible — at the top of your front controller or bootstrap file:

use Zharp\Logs\Zharp;

Zharp::init([
    'api_key' => 'YOUR_INGEST_ONLY_KEY',
    'service' => 'checkout-api',
    'environment' => 'production',
    'app_version' => '1.4.2',
]);

Or set the environment and call Zharp::init() with nothing:

ZHARP_API_KEY=your-ingest-only-key
ZHARP_SERVICE=checkout-api      # optional — defaults to the name in your composer.json
ZHARP_ENV=production            # optional — no default

You can mix the two: what you pass is used, and anything you leave out is read from the environment. In a script or command it prints one line to stderr; under a web server it prints nothing, since it starts on every request.

[Zharp] Logging active — service "checkout-api", environment "production"

With no key anywhere it starts nothing and never throws. 'quiet' => true silences the startup line; ZHARP_DISABLED=true turns the SDK off.

Logging

Zharp::debug('Cache miss', ['key' => 'user:42']);
Zharp::info('Request received', ['path' => '/checkout']);
Zharp::warn('Rate limit approaching', ['remaining' => 5]);
Zharp::error('Stripe webhook failed', ['event_id' => 'evt_123']);
Zharp::fatal('Database connection lost');

// Any level — PSR-3 names too — and optionally another service name for this one log:
Zharp::log('warning', 'Charge declined', ['amount' => 5000], 'payments');

The array is optional and is kept with the log. The PSR-3 level names — warning, notice, critical, alert, emergency — map to Zharp's five levels.

Exceptions

try {
    $gateway->charge($order);
} catch (Throwable $e) {
    Zharp::error('Payment failed', ['order_id' => $order->id, 'exception' => $e]);
    Zharp::captureException($e);   // the exception alone
    Zharp::error($e);              // the same
}

An exception — as the message, or under 'exception' in the array, where PSR-3 puts it — brings its class and full stack, previous exceptions included. The message reads RuntimeException: Card declined when the exception is the message.

Zharp::setContext(['user_id' => $user->id]);   // added to every log's context
Zharp::resetSessionId();                         // e.g. per job in a long-running worker

Uncaught exceptions and fatal errors

In plain PHP, from the moment init() succeeds, an uncaught exception is sent as a fatal log with its stack and type: "uncaughtException", then handled as before — by the exception handler you had set, or as PHP does (reported, exit code 255). A fatal error that ends the script without an exception — out of memory, a parse error in an include — is sent too, with type: "fatalError". Turn both off with 'capture_exceptions' => false.

Monolog and Symfony

Zharp\Logs\Monolog\ZharpHandler works with Monolog 2 and 3. Each record goes with its channel, context and extra, at the matching level; an exception under 'exception' in the context brings its stack.

use Zharp\Logs\Monolog\ZharpHandler;
use Zharp\Logs\Zharp;

Zharp::init();
$logger->pushHandler(new ZharpHandler());   // or new ZharpHandler(Level::Warning) for WARNING and up

Symfony

# config/services.yaml
services:
    Zharp\Logs\Monolog\ZharpHandler: ~

# config/packages/monolog.yaml
monolog:
    handlers:
        zharp:
            type: service
            id: Zharp\Logs\Monolog\ZharpHandler
// public/index.php and bin/console, before the kernel boots
\Zharp\Logs\Zharp::init();   // ZHARP_* from the environment

When logs are sent

PHP has no background threads, so logs wait in memory and are sent when the request ends — never in the middle of it.

Where it runsWhen logs are sent
PHP-FPM, LiteSpeedWhen the request ends, after the response has gone — visitors never wait on Zharp
php -S, artisan serveWhen the request ends; these can't finish a response early, so the request waits for the send
Laravel queue workers, OctaneAfter every job and every request
Scripts, commands, daemonsEvery 5 seconds (flush_interval), at once for error and fatal, and when the script ends
Zharp::flush();      // send what's queued now, waiting at most 5 seconds
Zharp::close();      // send it all and stop; logs after this are dropped until init() again
Zharp::isActive();   // whether init() succeeded and logs are being sent
info
Bounded, always

Sending what's left takes at most 5 seconds, and at most 2 when a script crashes. If Zharp can't be reached in that time, what's left is dropped rather than holding the process.

Options

init() optionEnvironment variableDefault
api_keyZHARP_API_KEY— (nothing starts without one)
serviceZHARP_SERVICE, then OTEL_SERVICE_NAMEThe name in your composer.json (Laravel: APP_NAME), else unknown-service
environmentZHARP_ENVNone (Laravel: APP_ENV)
app_versionZHARP_VERSIONNone
sourceZHARP_SOURCENone
endpointZHARP_ENDPOINThttps://api.zharp.io/api/v1
capture_exceptions—On in plain PHP; off in Laravel, which handles them itself
flush_interval—5 seconds, for long-running processes
max_batch_size—50
quiet—Off — on prints nothing at startup

See your logs

Open Logs in the dashboard sidebar and pick your service in the service dropdown. Click a line to see its context and stack.

The Logs page, showing lines from one service

To be told when something shows up, add a log monitor.

What a log carries

One log entry — an exception reported by Laravel
{
  "service": "checkout-api",
  "level": "error",
  "message": "Card declined",
  "timestamp": "2026-10-26T10:00:43.512Z",
  "environment": "production",
  "version": "1.4.2",
  "metadata": {
    "name": "RuntimeException",
    "stack": "RuntimeException: Card declined in /var/www/app/Services/Payments.php:42\nStack trace:\n#0 …",
    "exception": { "name": "RuntimeException", "message": "RuntimeException: Card declined", "stack": "…" },
    "channel": "production",
    "sessionId": "a3f8c2d1-…",
    "environment": "production",
    "context": {
      "hostname": "web-1",
      "phpVersion": "8.3.12",
      "platform": "Linux",
      "sapi": "fpm-fcgi",
      "pid": "4182",
      "method": "POST",
      "path": "/checkout",
      "appVersion": "1.4.2"
    }
  }
}
  • environment, version and source are top-level tags, sent when set. See Unified service tagging.
  • Your own fields sit at the top of metadata, next to an exception's name and stack, the Monolog channel, and the sessionId — one per request or script run.
  • Where it ran goes under context: hostname, phpVersion, platform, sapi and pid, and for a web request its method and path — never the query string, which can carry tokens.
  • Requests go to /agent/logs with the key in an X-API-KEY header and X-Zharp-SDK: php-logs/0.1.0. A failure on the network or a 5xx is retried, within the time limit; a 4xx is not.

The source is at github.com/zharp-io/zharp-php-lib.