Skip to content

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

  • PHP 7.4 or newer, with the curl extension. 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.

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"
}
}
Terminal window
composer update loghero/sdk

Without Composer, clone the repository and require its autoloader:

require_once '/path/to/lh-php-sdk/autoload.php';

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.

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.

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);

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.

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')
);

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 502 or 504 from 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.

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.

Work through No data arriving. The SDK-specific causes:

  • PermissionDeniedException on 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.