PHP SDK
The PHP SDK is the integration for applications that are not WordPress: a Laravel or Symfony project, a legacy PHP codebase, anything where you can add a few lines to the request lifecycle. It is the same client the WordPress plugin is built on, so the buffering and delivery behaviour is identical.
If you run WordPress, install the plugin instead. It does all of this for you.
Time: about 30 minutes · Access needed: the application’s code, deploy access
Before you start
Section titled “Before you start”- PHP 7.4 or newer, with the
curlextension. The SDK uses curl directly to talk to the API. - Your Log Hero API key, from the signup flow at log-hero.com.
- A writable directory for the buffer file, outside your document root, or a Redis server.
1. Install the SDK
Section titled “1. Install the SDK”The SDK is not on Packagist. Point Composer at the repository directly:
{ "repositories": [ { "type": "vcs", "url": "https://github.com/LogHero/lh-php-sdk" } ], "require": { "loghero/sdk": "^0.6" }}composer update loghero/sdkWithout Composer, clone the repository and require its autoloader:
require_once '/path/to/lh-php-sdk/autoload.php';2. Record a log event per request
Section titled “2. Record a log event per request”Add this at the end of the request, once the status code is known. The factory
reads everything it needs out of $_SERVER, so there is nothing to pass in.
use LogHero\Client\APISettings;use LogHero\Client\APIAccess;use LogHero\Client\FileLogBuffer;use LogHero\Client\AsyncLogTransport;use LogHero\Client\LogEventFactory;
$apiKey = getenv('LOGHERO_API_KEY');$clientId = 'acme-loghero-client';$flushTriggerEndpoint = 'https://example.com/loghero-flush.php';
$logBuffer = new FileLogBuffer('/var/lib/loghero/buffer.loghero.io.txt');$apiAccess = new APIAccess($clientId, new APISettings($apiKey));$logTransport = new AsyncLogTransport( $logBuffer, $apiAccess, $clientId, $apiKey, $flushTriggerEndpoint);
$logTransport->submit((new LogEventFactory())->create());Pick a $clientId that identifies your application, for example
<your-company>-loghero-client. It is sent as the User-Agent when the SDK calls
your own flush endpoint, which lets you exclude those calls from your logs.
Where this code goes depends on your stack:
| Stack | Where to call submit() |
|---|---|
| Plain PHP | register_shutdown_function(), or the last lines of your front controller |
| Laravel | A terminable middleware, in its terminate() method |
| Symfony | An event listener on kernel.terminate |
Anything registered on shutdown or termination runs after the response has been sent, so the visitor never waits for it.
3. Add the flush endpoint
Section titled “3. Add the flush endpoint”AsyncLogTransport does not send anything itself. When the buffer needs
dumping it calls the URL you configured as $flushTriggerEndpoint, and that
endpoint does the work. Create it as a small standalone script:
<?php// Answer and close the connection first, then flush in the background.ignore_user_abort(true);set_time_limit(0);ob_start();header('Connection: close');header('Content-Length: ' . ob_get_length());ob_end_flush();flush();if (session_id()) { session_write_close();}
require_once __DIR__ . '/vendor/autoload.php';
use LogHero\Client\APISettings;use LogHero\Client\APIAccess;use LogHero\Client\FileLogBuffer;use LogHero\Client\AsyncLogTransport;
$apiKey = getenv('LOGHERO_API_KEY');$clientId = 'acme-loghero-client';
// The trigger sends the API key in a Token header. Reject anything else.if (!hash_equals($apiKey, $_SERVER['HTTP_TOKEN'] ?? '')) { http_response_code(403); exit;}
$apiAccess = new APIAccess($clientId, new APISettings($apiKey));$logBuffer = new FileLogBuffer('/var/lib/loghero/buffer.loghero.io.txt');(new AsyncLogTransport($logBuffer, $apiAccess, $clientId, $apiKey, null)) ->dumpLogEvents();Both scripts must point at the same buffer file, and the endpoint must be reachable from the server itself. If your firewall blocks loopback requests, use the synchronous transport instead.
Synchronous instead
Section titled “Synchronous instead”If the asynchronous path is more machinery than you want, swap the transport and drop the endpoint. Records are then sent inside the request that fills the buffer, which adds the round trip to the API to roughly one request in a thousand.
use LogHero\Client\LogTransport;
$logTransport = new LogTransport($logBuffer, $apiAccess);4. Confirm hits are arriving
Section titled “4. Confirm hits are arriving”Deploy, then open Log Hero → Bot Activity in your Keyword Hero account. Within a few minutes you should see the banner Log Hero is receiving bot traffic for this domain and a non-zero Bot hits figure.
The very first records can lag by up to five minutes: a quiet site does not fill the buffer, so it waits for the time limit instead.
Buffers and thresholds
Section titled “Buffers and thresholds”The buffer exists so that neither your server nor the API sees one HTTP call per request. Records accumulate and go out as one batch, at most 1000 records per batch.
| File buffer | Redis buffer | |
|---|---|---|
| Class | FileLogBuffer |
RedisLogBuffer |
| Flushes when | the file passes 100 KB | 1000 records are queued |
| Or after | 5 minutes | 5 minutes |
| Hard limit | 5 MB, then records are dropped | 1,000,000 records, then trimmed |
| Use it when | one application server | several servers, or a read-only filesystem |
The hard limit is a safety net: if delivery is broken, the buffer stops growing instead of filling the disk. Records collected past that point are lost.
use LogHero\Client\RedisLogBuffer;use LogHero\Client\RedisOptions;
$logBuffer = new RedisLogBuffer( new Predis\Client('tcp://127.0.0.1:6379'), new RedisOptions('tcp://127.0.0.1:6379', 'io.loghero:acme'));What the SDK cannot see
Section titled “What the SDK cannot see”The SDK runs inside your application, so it only sees requests your application handles.
- Static files served straight off disk by nginx or Apache never reach PHP.
- Cached responses answered by a reverse proxy or CDN in front of the application are invisible.
- The recorded duration is how long PHP took, measured from
REQUEST_TIME_FLOAT, not what the visitor experienced. - Server errors that never reached PHP — a
502or504from the web server when the PHP pool was down or timed out. Errors PHP produced itself, fatal ones included, are recorded. See Status codes.
To capture static files as well, log at the web server instead: see Apache or, behind a CDN, Choose your integration.
What is sent
Section titled “What is sent”One record per request, for every request, from bots and human visitors alike:
hostname, protocol, path with query string, method, status code, User-Agent,
Referer, client IP address, timestamp and PHP response time — see
Anatomy of a log line for what each field
is worth. Records go to https://in.app.log-hero.com/logs.
Bot filtering happens on the Log Hero side, so visitor IP addresses are transmitted too.
If it doesn’t work
Section titled “If it doesn’t work”Work through No data arriving. The SDK-specific causes:
PermissionDeniedExceptionon the first request: the buffer path is not writable by the web server user, see Permission denied.AsyncFlushFailedException: the flush endpoint is unreachable or answered with an error, see Asynchronous flush failed.- A growing buffer file and no data: the two scripts point at different buffer files, or the endpoint rejects the token.
The SDK never throws into your application on its own. Exceptions surface only
if you let them, so wrap the submit() call in a try/catch and log to your
own error log while you are setting this up.
