getfaultline / faultline-php
PHP SDK for Faultline error tracking, with a Laravel integration and a PSR-15 middleware
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- justinrainbow/json-schema: ^5.3 || ^6.0
- nyholm/psr7: ^1.8
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
- psr/http-server-middleware: ^1.0
- psr/log: ^2.0 || ^3.0
Suggests
- ext-apcu: Shares the 429 Retry-After deadline across PHP-FPM workers without touching the filesystem
- ext-zlib: Compresses event batches with gzip before sending
- psr/http-server-middleware: Required for Faultline\Integrations\Psr15\FaultlineMiddleware (Slim, Mezzio)
- psr/log: Required for Faultline\Integrations\Psr3\BreadcrumbLogger
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-13 15:17:45 UTC
README
The PHP SDK for Faultline, a self-hostable error-tracking platform. It captures exceptions, PHP errors and messages, adds context (stack traces with source, breadcrumbs, request data, user, tags), scrubs secrets, and sends batched, gzip-compressed events to the Faultline ingest API.
- No runtime dependencies. Needs only
ext-jsonandext-curl.ext-zlib(gzip) andext-apcu(shared rate-limit state) are used when present. - Never crashes your app. Every public method is exception-safe, and internal errors are
logged only when
debug: true. - Never slows your requests. Events are queued in memory and sent in one batch at
shutdown, after
fastcgi_finish_request()under PHP-FPM, so the user does not wait. - Laravel integration (auto-discovered) and a PSR-15 middleware for Slim and Mezzio.
- PHP >= 8.1.
Quickstart
composer require getfaultline/faultline-php
require __DIR__.'/vendor/autoload.php'; \Faultline\init([ 'apiKey' => getenv('FAULTLINE_API_KEY'), // flt_ingest_... 'environment' => 'production', 'release' => 'shop@1.4.2', ]); try { doRiskyThing(); } catch (\Throwable $e) { \Faultline\captureException($e, ['tags' => ['feature' => 'checkout']]); } \Faultline\captureMessage('Payment webhook retried', 'warning');
That is all a plain PHP app needs: no middleware. init() installs an exception handler, an
error handler and a fatal-error shutdown handler, so uncaught exceptions, warnings and fatal
errors are reported automatically. Everything is sent at the end of the request.
Every function is also available as a static method: \Faultline\Faultline::init([...]),
Faultline::captureException($e), and so on.
A runnable script is in examples/plain.php:
FAULTLINE_API_KEY=flt_ingest_... FAULTLINE_ENDPOINT=http://localhost:4000 php examples/plain.php
CLI scripts and workers
There is nothing extra to do for a script: the queue is flushed when it exits. Call
\Faultline\flush() to send immediately, for example before a long sleep(). In CLI
processes the queue is also sent automatically once it holds 20 events, so long-running
workers never hold events for long. The Laravel integration flushes between queue jobs.
Laravel
The service provider is auto-discovered. Add your key to .env:
FAULTLINE_API_KEY=flt_ingest_... # optional FAULTLINE_ENDPOINT=http://localhost:4000 # self-hosted server FAULTLINE_RELEASE=shop@1.4.2 FAULTLINE_ENVIRONMENT=production # defaults to APP_ENV FAULTLINE_SAMPLE_RATE=1.0 FAULTLINE_SEND_DEFAULT_PII=false FAULTLINE_DEBUG=false
Optionally publish the config file to config/faultline.php:
php artisan vendor:publish --tag=faultline-config
What you get:
- Exceptions are reported through Laravel's exception handler with a
reportable()callback, sodontReport,shouldReport()and report throttling are respected (404s and validation errors are not sent). Laravel keeps logging as usual. - Request data: method, URL, headers (scrubbed) and query string, plus the route name, URI
and action in
contexts.routeand aroutetag. - User: the authenticated user's id and email (and IP with
send_default_pii). - Breadcrumbs: log messages (
log), executed SQL (query, without bindings unlesssend_default_pii) and the matched route (navigation). - Delivery after the response was sent (
terminating), and between queue jobs. Each job gets its own breadcrumbs and ajobcontext.
Without FAULTLINE_API_KEY the SDK stays disabled, which is handy for local development.
Capture manually anywhere with \Faultline\captureException($e). The config keys are
snake_case versions of the options below (api_key, sample_rate, ignore_errors,
before_send, ...), plus breadcrumbs.logs and breadcrumbs.queries. before_send takes a
container-resolved callable: an invokable class name, "Class@method" or
[Class::class, 'method'], so php artisan config:cache keeps working.
Slim, Mezzio and other PSR-15 apps
use Faultline\Integrations\Psr15\FaultlineMiddleware; \Faultline\init(['apiKey' => getenv('FAULTLINE_API_KEY')]); $app = Slim\Factory\AppFactory::create(); $app->addErrorMiddleware(true, true, true); $app->add(new FaultlineMiddleware()); // added last, so it runs first and sees the exception
The middleware forks a scope per request, attaches request data and a navigation
breadcrumb, captures any Throwable that escapes the inner handler, and re-throws it
unchanged. It needs psr/http-server-middleware.
PSR-3: wrap any logger with new Faultline\Integrations\Psr3\BreadcrumbLogger($logger) to
turn log calls into log breadcrumbs (needs psr/log 2 or 3).
API
| Function | Description |
|---|---|
init(array $options) |
Configure the SDK. Calling it again replaces the previous client. |
captureException(Throwable $e, ['tags', 'extra', 'user', 'level', 'fingerprint', 'contexts']) |
Capture an exception. Returns the eventId, or null when the event was dropped or the SDK is disabled. |
captureMessage(string $message, string $level = 'info') |
Capture a message. The second argument may also be a capture context array. |
setUser(['id', 'email', 'username', 'ipAddress']) / setUser(null) |
Set or clear the user on the current scope. |
setTag($key, $value) / setTags([...]) |
Searchable string tags. |
setContext($name, [...]) / setExtra($key, $value) |
Structured context or arbitrary extra data. |
addBreadcrumb(['type', 'category', 'message', 'level', 'data']) |
Manual breadcrumb. type is one of default, http, log, query, navigation. |
withScope(fn (Scope $scope) => ...) |
Run code with a forked scope. Changes apply only inside the callback. Returns the callback's result. |
flush(int $timeoutMs = 2000) |
Send queued events now. Returns false on timeout and never throws. |
close(int $timeoutMs = 2000) |
Flush, remove the handlers, and disable the SDK. |
Faultline::isEnabled() / Faultline::lastEventId() |
true once init has succeeded / id of the last queued event. |
setUser, setTag, setContext, setExtra and addBreadcrumb write to the current
scope. Outside withScope (or the PSR-15 middleware) that is the global scope, which in PHP
lives for exactly one request.
Options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
required | Public ingest key (flt_ingest_...). Sent as X-Faultline-Key. |
endpoint |
string |
https://api.faultsline.dev |
Base URL of the Faultline server. Override it when self-hosting. |
environment |
string |
- | e.g. production, staging. |
release |
string |
- | e.g. shop@1.4.2 or a git sha. |
serverName |
string |
gethostname() |
Reported as serverName. |
sampleRate |
float 0..1 |
1.0 |
Fraction of events to send. |
maxBreadcrumbs |
int |
50 |
Breadcrumbs kept per scope (capped at 100). |
sendDefaultPii |
bool |
false |
When false, the user IP and the cookie, set-cookie, authorization, proxy-authorization, x-forwarded-for, x-real-ip and forwarded headers are removed, and SQL bindings are not recorded. |
beforeSend |
callable(array $event): ?array |
- | Modify or drop (null) an event. Runs after scrubbing. If it throws, the event is dropped. |
ignoreErrors |
list<string> |
[] |
Class names (subclasses match too, via instanceof) or regex strings such as '/timeout/i', tested against the exception type, the message and "Type: message". |
integrations |
array |
['exceptionHandler' => true, 'errorHandler' => true, 'fatalErrorHandler' => true] |
Toggle the PHP handlers (see below). |
enabled |
bool |
true |
false turns the SDK into a no-op (apiKey is then not required). |
debug |
bool |
false |
Log SDK internals (dropped events, retries, invalid options) with error_log(). |
Invalid options never throw:
- An invalid optional value falls back to its default.
- A missing
apiKeyor an unusableendpointdisables the SDK. - Run with
debug: trueto see why.
PHP handlers
-
exceptionHandler:
set_exception_handler(). Uncaught exceptions are captured at levelfatal. A previously registered handler is chained and keeps the final say; without one, the exception is re-thrown so PHP prints its usual message and exits with 255. -
errorHandler:
set_error_handler(). Errors silenced with@or excluded byerror_reporting()are ignored. The previous handler is always called, and PHP's standard handler still runs, so display and logging are unchanged.PHP error Becomes E_WARNING,E_USER_WARNINGevent ( ErrorException), levelwarningE_USER_ERROR,E_RECOVERABLE_ERRORevent ( ErrorException), levelerrorE_NOTICE,E_USER_NOTICE,E_DEPRECATED,E_USER_DEPRECATEDlogbreadcrumb -
fatalErrorHandler:
register_shutdown_function()+error_get_last()forE_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERRORand friends (typeFatalError, levelfatal). A small memory reserve is released first, and after "Allowed memory size exhausted" the limit is raised slightly so the event can still be built and sent.
Self-hosting
Set endpoint to your server's base URL. The SDK posts to {endpoint}/api/v1/ingest/events:
\Faultline\init(['apiKey' => 'flt_ingest_...', 'endpoint' => 'http://localhost:4000']); // local faultline-server \Faultline\init(['apiKey' => 'flt_ingest_...', 'endpoint' => 'https://faultline.internal.example.com']);
In Laravel, set FAULTLINE_ENDPOINT.
What gets sent
Each event follows the shared Faultline event contract: eventId (uuid v4), timestamp,
level, platform: "php", exception (type = class name, value, frames ordered
oldest → newest, the getPrevious() chain as cause), message, tags, user,
request, contexts (runtime, os, plus your own), breadcrumbs, extra,
fingerprint, environment, release, serverName, and
sdk: { name: "getfaultline/faultline-php", version }.
Frames come from Throwable::getTrace() plus the throw location, with function names like
App\Http\Controllers\OrderController->show or App\Support\Str::slug. Frames outside
/vendor/ are marked inApp and get ±5 lines of source context.
The SDK enforces the pinned limits before sending:
| Field | Limit |
|---|---|
| Frames | 100 (newest kept) |
| Breadcrumbs | 100 |
| Tags | 50 (key ≤ 32 chars, value ≤ 200 chars) |
message / exception.value |
8192 chars |
| Source context | ±5 lines, each ≤ 300 chars |
exception.cause chain |
5 exceptions in total |
| Batch | ≤ 50 events and ≤ 1 MB |
Scrubbing (client side, same rules as the server and the Node SDK):
- Values under keys matching
/(authorization|cookie|set-cookie|password|passwd|secret|token|api[_-]?key|x-faultline-key)/ibecome"[Filtered]". This applies to request headers and query,extra,contexts, tags and breadcrumb data. - Credit-card-like strings in request,
extraand breadcrumb data are also filtered, as are sensitive URL query parameters.
Delivery:
capture*()only queues the event (max 100, oldest dropped). Nothing touches the network while your code runs.- At shutdown the queue is sent in gzip-compressed batches of at most 50 events / 1 MB. Under
PHP-FPM (and LiteSpeed) the response is finished first with
fastcgi_finish_request(). - Each request times out after 5s (2s to connect).
5xx,408and network errors are retried with exponential backoff and jitter, within the flush deadline. - A
429stores itsRetry-Afterdeadline in APCu (when enabled) or a temp file, so every PHP-FPM worker skips sends until then. - A
401or413response drops the batch. Withdebug: truethe SDK logs the reason. - Identical consecutive errors within 1s are sent once.
Architecture
The SDK follows Clean Architecture. Dependencies point inward only, and
tests/Unit/ArchitectureTest.php enforces this in CI.
flowchart LR
subgraph facade["Faultline (public API · composition root) + functions.php"]
end
subgraph integrations["Integrations/"]
P[Php handlers] --- L[Laravel] --- M[PSR-15] --- G[PSR-3]
end
subgraph infrastructure["Infrastructure/"]
T[HttpTransport + curl] --- S[PhpStackTraceBuilder] --- F[FileSourceContextReader] --- R[rate-limit stores · runtime]
end
subgraph application["Application/"]
CL[Client] --- HB[Hub] --- PO[Ports: Transport, StackTraceBuilder, SourceContextReader, Clock, Logger, ...]
end
subgraph domain["Domain/ (pure PHP)"]
EV[Event contract] --- SC[Scope] --- OP[Options validation] --- SR[scrubbing · limits · normalization]
end
facade --> integrations
facade --> infrastructure
integrations --> application
infrastructure --> application
application --> domain
Loading
sequenceDiagram participant App participant Hub participant Client participant Transport as HttpTransport participant Server as Faultline ingest App->>Hub: captureException($e) Hub->>Client: $e + current scope Client->>Client: frames · previous chain · sample · ignoreErrors · dedupe Client->>Client: source context · limits · scrub · beforeSend Client->>Transport: send(event) (queued in memory) Client-->>App: eventId Note over App,Transport: request ends · fastcgi_finish_request() Transport->>Server: POST /api/v1/ingest/events (gzip batch ≤ 50) Server-->>Transport: 202 / 429 Retry-After / 401 / 413Loading
See ARCHITECTURE.md for the decision log.
Development
| Command | What it does |
|---|---|
composer install |
Install dev dependencies. |
composer cs |
PHP-CS-Fixer dry run (PSR-12 + strict types). composer cs:fix applies it. |
composer analyse |
PHPStan: src, config and examples at level max, tests at level 8. |
composer test |
PHPUnit: unit, integration (a php -S mock ingest server and real PHP subprocesses) and Laravel (Testbench). |
composer check |
All of the above. |
Every produced payload is validated against tests/Fixtures/event.schema.json, a test-only
JSON Schema that mirrors the shared contract.
The SDK reads no environment variables of its own; everything is passed to init. The
example script and the Laravel config read FAULTLINE_API_KEY and FAULTLINE_ENDPOINT.
Publishing
Packagist only publishes packages from a public Git repository. To release:
- Push this repository to a public host, e.g.
github.com/getfaultline/faultline-php. - Submit the repository URL at packagist.org/packages/submit (once), and enable the GitHub hook so new tags are picked up automatically.
- Tag a release:
git tag v0.1.0 && git push --tags. Packagist versions come from tags;composer.jsonhas noversionfield on purpose.
License
MIT