Search by

getfaultline / faultline-php

Meshachkings

PHP SDK for Faultline error tracking, with a Laravel integration and a PSR-15 middleware

Package info

github.com/getfaultline/faultline-php

Homepage

pkg:composer/getfaultline/faultline-php

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-10-13 14:47 UTC

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-json and ext-curl. ext-zlib (gzip) and ext-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, so dontReport, 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.route and a route tag.
  • User: the authenticated user's id and email (and IP with send_default_pii).
  • Breadcrumbs: log messages (log), executed SQL (query, without bindings unless send_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 a job context.

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 apiKey or an unusable endpoint disables the SDK.
  • Run with debug: true to see why.

PHP handlers

  • exceptionHandler: set_exception_handler(). Uncaught exceptions are captured at level fatal. 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 by error_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_WARNING event (ErrorException), level warning
    E_USER_ERROR, E_RECOVERABLE_ERROR event (ErrorException), level error
    E_NOTICE, E_USER_NOTICE, E_DEPRECATED, E_USER_DEPRECATED log breadcrumb
  • fatalErrorHandler: register_shutdown_function() + error_get_last() for E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR and friends (type FatalError, level fatal). 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)/i become "[Filtered]". This applies to request headers and query, extra, contexts, tags and breadcrumb data.
  • Credit-card-like strings in request, extra and 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, 408 and network errors are retried with exponential backoff and jitter, within the flush deadline.
  • A 429 stores its Retry-After deadline in APCu (when enabled) or a temp file, so every PHP-FPM worker skips sends until then.
  • A 401 or 413 response drops the batch. With debug: true the 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 / 413
Loading

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:

  1. Push this repository to a public host, e.g. github.com/getfaultline/faultline-php.
  2. Submit the repository URL at packagist.org/packages/submit (once), and enable the GitHub hook so new tags are picked up automatically.
  3. Tag a release: git tag v0.1.0 && git push --tags. Packagist versions come from tags; composer.json has no version field on purpose.

License

MIT