Flare by Spatie
    • Error Tracking
    • Performance Monitoring
    • Logs Coming soon
  • Pricing
  • Docs
  • Insights
  • Changelog
  • Back to Flare ⌘↵ Shortcut: Command or Control Enter
  • Sign in
  • Try Flare for free
  • Error Tracking
  • Performance Monitoring
  • Logs Coming soon
  • Pricing
  • Docs
  • Insights
  • Changelog
    • Back to Flare ⌘↵ Shortcut: Command or Control Enter
    • Try Flare for free
    • Sign in
Flare Flare Laravel Laravel PHP PHP JavaScript JavaScript React React Vue Vue Protocol Protocol
  • General
  • Installation
  • Integrating into a framework
  • Attribute providers
  • Application lifecycle
  • Censoring collected data
  • Ignoring collected data
  • Flare daemon
  • Errors
  • Adding custom context
  • Customise error report
  • Customising error grouping
  • Handling errors
  • Linking to errors
  • Reporting errors
  • Logs
  • Introduction
  • Levels
  • With errors
  • Performance
  • Introduction
  • Sampling
  • Limits
  • Modify spans and events
  • Data Collection
  • Application info
  • Cache events
  • Console commands
  • Database transactions
  • Dumps
  • Errors when tracing
  • Exception context
  • External http requests
  • Filesystem operations
  • Git information
  • Glows
  • Identifying users
  • Jobs and queues
  • Queries
  • Redis commands
  • Requests
  • Routing
  • Server info
  • Spans
  • Stacktrace arguments
  • Views
  • Older Packages
  • Flare Client PHP V2
  • Flare Client PHP V1

Integrating into a framework

spatie/flare-client-php is the framework-agnostic core that all Flare PHP integrations are built on. If you maintain a framework integration (or you want to wire Flare into a custom application), this guide shows the call sequence the client expects.

If you use Laravel, install spatie/laravel-flare instead. It implements everything described here for you.

Building blocks

Before getting into the wiring, here is the vocabulary the rest of this guide uses.

Configuration and bootstrap

  • FlareConfig. The user-facing configuration object that your framework's config maps onto.
  • FlareProvider. Wires every Flare service onto your container in two phases: register() and boot().
  • Container. Flare's PSR-11 container. Bring your own if your framework already has one.
  • Flare. The runtime object you hand around your application after boot().

Tracing and sampling

  • Tracer. Produces traces and spans. Recorders call into it.
  • Sampler. Decides which traces are sampled. See Sampling.
  • EntryPoint and EntryPointResolver. Describe the request, command, or job that initiated the current work. See Entry points.
  • Lifecycle. Manages the trace lifecycle stages and subtask mode. See Application lifecycle.

Data collection

  • Recorder. Observes runtime work and produces spans or span events. See Spans.
  • Middleware. Decorates an error report inside the report pipeline. See Customise error report.
  • AttributesProvider. Turns a framework object (request, command input, job) into the attribute array a recorder records. See Attribute providers.
  • CollectsResolver. Translates the collects configuration array on FlareConfig into recorder, middleware, and resource registrations.

Errors and logs

  • Logger. The standalone log recorder, accessed as $flare->log(). See Logs.
  • Reporter. Sends error reports through the middleware pipeline.
  • ReportFactory. The in-flight error report that middleware decorates before it's flushed.
  • SentReports. A registry of reports sent during the current request, used for tests and "view in Flare" links.

Transport and metadata

  • Sender. The transport at the bottom of the client. Implements a single post() method that ships a payload (error, trace, or log batch). Built-in: CurlSender (default), DaemonSender, GuzzleSender, NullSender. See the Senders section below for switching senders or writing your own.
  • Api. Wraps the Sender with queueing and retries.
  • Resource. The long-lived application descriptor (service, host, OS, runtime, git, SDK). See Resources.
  • Scope. The instrumentation scope (telemetry SDK name and version). See Scope.

Bootstrap

Framework integrations build a Flare instance themselves through FlareProvider.

The flow is the same in every integration: build a FlareConfig, instantiate FlareProvider with your container(see later), run its register() and boot() phases, and resolve Flare out of the container.

use Spatie\FlareClient\Flare;
use Spatie\FlareClient\FlareConfig;
use Spatie\FlareClient\Support\Container;
use Spatie\FlareClient\FlareProvider;

$config = FlareConfig::make('YOUR-API-KEY')
    ->useDefaults()
    ->applicationPath('/path/to/your/application/root');
    
$container = Container::instance();

$provider = new FlareProvider($config, $container);

$provider->register();
$provider->boot();

$flare = $container->get(Flare::class);

$flare->registerFlareHandlers();

useDefaults() enables the standard set of recorders (requests, queries, jobs, queues, views, filesystem, Redis commands, cache events, glows, and so on). You can opt out per recorder with the matching ignore* methods on the config, or skip useDefaults() and enable recorders one by one.

Register and boot phases

FlareProvider is modeled after Laravel's service provider lifecycle, even when used outside of Laravel. There are two phases:

  • register() binds every Flare service onto the container as a singleton: the Sender, Api, Sampler, Resource, Scope, EntryPointResolver, Tracer, Logger, Reporter, Lifecycle, every recorder, every middleware, and finally Flare itself. Nothing is instantiated yet, so this phase is cheap and side-effect free. This is also the phase where your framework integration gets to override a binding (for example, to register a framework-specific request attribute provider) before anything is resolved.
  • boot() asks the recorders to register themselves with the framework hooks they need (event listeners, middleware, dispatcher overrides, etc.). Recorders that need to react to framework events do that here, after every binding is in place and resolvable.

If apiToken is empty on the config, FlareProvider::$mode is set to FlareMode::Disabled and boot() becomes a no-op so a missing key never crashes a production deploy and thus will never send data to Flare.

Bringing your own container

FlareProvider's constructor accepts either Flare's built-in container (Spatie\FlareClient\Support\Container) or any PSR-11 container that also exposes the two methods Flare uses to register bindings:

  • singleton(string $class, Closure|string|array|null $builder = null): void
  • bind(string $class, Closure|null $builder = null): void

Plus reflection-based autowiring: when a class is requested that has not been bound, the container should look at the constructor signature and resolve each parameter through itself. Flare's recorders rely on this so framework integrations can pass ['config' => $config] to a recorder and have the rest of the constructor parameters pulled from the container automatically.

If your framework doesn't have a container at all yet, you can fall back to Flare's global instance:

use Spatie\FlareClient\Support\Container;

$container = Container::instance();

Extending FlareConfig

For most frameworks, FlareConfig covers the standard options. When your framework adds settings of its own, the recommended path is to extend FlareConfig rather than carry the framework state somewhere else.

A subclass lets you:

  • Add framework-specific properties alongside the built-in ones, and pass them through the constructor in a single call.
  • Provide a static factory that reads your framework's config files. spatie/laravel-flare uses this pattern via FlareConfig::fromLaravelConfig(), which reads config('flare.*') and returns a fully built config object.
  • Override the defaultCollects() method (when present) to seed the collect list with framework-specific recorders before user code calls useDefaults().

Custom CollectsResolver

CollectsResolver is the bridge between the collects array on FlareConfig and the actual recorder / middleware / resource-modifier registrations on the container. When your framework needs to register recorders or middleware that the base resolver doesn't know about, extend CollectsResolver.

Two extension points cover most cases:

  • handleUnknownCollectType(FlareCollectType $type, array $options): void. The base resolver calls this for any CollectType it doesn't recognize. Use it to wire up your own enum-based collect types alongside the built-in ones.
  • Override the standard collect handlers (requests(), console(), jobs(), cache(), views(), etc.) to swap in framework-specific recorders or middleware. Call parent::xxx($options) first if you only want to add to the base behavior, or skip the parent call to replace it.
namespace YourFramework\Flare\Support;

use Spatie\FlareClient\Contracts\FlareCollectType;
use Spatie\FlareClient\Support\CollectsResolver as BaseCollectsResolver;
use YourFramework\Flare\Enums\YourFrameworkCollectType;
use YourFramework\Flare\FlareMiddleware\AddYourFrameworkInformation;
use YourFramework\Flare\Recorders\YourFrameworkRecorder;

class CollectsResolver extends BaseCollectsResolver
{
    protected function handleUnknownCollectType(FlareCollectType $type, array $options): void
    {
        match ($type) {
            YourFrameworkCollectType::FrameworkInfo => $this->frameworkInfo($options),
            default => null,
        };
    }

    protected function frameworkInfo(array $options): void
    {
        $this->addMiddleware(AddYourFrameworkInformation::class);
        $this->addRecorder(YourFrameworkRecorder::class, $options);
    }

    protected function requests(array $options): void
    {
        // Swap in a framework-specific request middleware while keeping the
        // base RequestRecorder / RoutingRecorder / ResponseRecorder wiring.
        $options['middleware'] ??= \YourFramework\Flare\FlareMiddleware\AddRequestInformation::class;

        parent::requests($options);
    }
}

Register the custom resolver through the collectsResolver config option so FlareProvider instantiates yours instead of the base class:

$config->collectsResolver = YourFramework\Flare\Support\CollectsResolver::class;

Customizing FlareProvider

FlareProvider accepts four optional constructor arguments that control how it interacts with your framework. The defaults work for a plain PHP app, but most framework integrations override at least one of them.

$provider = new FlareProvider(
    config: $config,
    container: $container,
    registerRecorderAndMiddlewaresCallback: $registerRecorderAndMiddlewaresCallback,
    isUsingSubtasksClosure: $isUsingSubtasksClosure,
    gracefulSpanEnderClosure: $gracefulSpanEnderClosure,
    disableApiQueue: $disableApiQueue,
);

registerRecorderAndMiddlewaresCallback

Signature: Closure(Container, class-string<Recorder>, array $config): void.

Called once per recorder and once per middleware during register(), with the container, the class name, and the option array from the resolver. The default callback binds the class as a singleton and passes ['config' => $config] so the autowiring layer can pull the rest of the constructor parameters from the container.

Override it when your container needs a different binding API. The Laravel integration uses contextual binding ($app->when($class)->needs('$config')->give($config)) and also invokes a static registered($container, $config) hook on the recorder if it exists, so third-party recorders can run additional setup at register time.

$registerRecorderAndMiddlewaresCallback = function ($container, string $class, array $config) {
    $container->singleton($class);
    $container->when($class)->needs('$config')->give($config);

    if (method_exists($class, 'registered')) {
        $class::registered($container, $config);
    }
};

isUsingSubtasksClosure

Signature: Closure(): bool.

Returns true when the lifecycle should run in subtask mode (one persistent process handling many units of work) instead of starting a fresh root trace per request. Lifecycle calls this once at construction and stores the result on the readonly usesSubtasks property.

This is the only switch that flips subtask mode on, so every long-running runtime your integration supports needs to be detected here.

gracefulSpanEnderClosure

Signature: Closure(Span $span): bool.

Tracer::gracefullyEndSpans() runs from the registered shutdown function and force-closes any span still open at the end of the request. The closure decides, per span, whether that span should actually be closed. Return true to let the tracer end it (the default), or false to leave it open.

This matters for spans that your integration closes itself in a later phase. Laravel keeps php_application, php_request, and php_application_terminating open across the shutdown handler so Lifecycle::terminated() can close them with the correct end time:

use Spatie\FlareClient\Enums\SpanType;

$gracefulSpanEnderClosure = function (Span $span) {
    $type = $span->attributes['flare.span_type'] ?? null;

    if ($type === null) {
        return true;
    }

    return $type !== SpanType::Application
        && $type !== SpanType::Request
        && $type !== SpanType::ApplicationTerminating;
};

disableApiQueue

Signature: bool.

Disables the API request queue inside the Api client. With the queue off, errors and logs are sent to Flare immediately on each call instead of being batched and flushed later. Use this for one-shot processes that won't outlive the queue (REPLs, short test commands, or anywhere you need telemetry to land synchronously). Laravel sets it to true when running under php artisan tinker.

What you get back

After boot() returns, resolve Flare::class from the container to get a fully constructed Flare. The building blocks above are exposed as public readonly properties: $flare->lifecycle, $flare->tracer (plus $flare->backTracer), $flare->logger (also $flare->log()), $flare->reporter, and $flare->sentReports.

Each recorder is a method on $flare: request(), routing(), command(), job(), queue(), query(), and so on. They return null when the matching recorder is disabled, so call them with the null-safe operator ($flare->request()?->recordStart(...)).

Wiring the application lifecycle

A framework integration is responsible for telling Flare when the application starts, registers, boots, terminates. The full sequence of methods is documented in Application lifecycle; here is the typical wiring for a framework that runs through a single request and then exits.

use Spatie\FlareClient\Time\TimeHelper;

$startTime = TimeHelper::phpMicroTime($_SERVER['REQUEST_TIME_FLOAT']);

$flare->lifecycle->start(
    timeUnixNano: $startTime,
    traceparent: $request->getHeader('traceparent'),
);

$flare->lifecycle->register();
// ... register services in your container ...
$flare->lifecycle->registered();

$flare->lifecycle->boot();
// ... boot service providers ...
$flare->lifecycle->booted();

// ... handle the request ...

$flare->lifecycle->terminating();
// ... run terminate callbacks ...
$flare->lifecycle->terminated();

For a long-running process that handles many units of work (a queue worker, an Octane-style HTTP server), see the subtask mode section below.

A register_shutdown_function is registered automatically during Lifecycle construction, so any pending payloads are flushed even if the PHP process exits unexpectedly.

Recording the request

The request recorder produces the php_request container span and sets the entry point to web. Pass a RequestAttributesProvider to recordStart(), or use one of the helpers:

// From a Symfony / Laravel HttpFoundation request
$flare->request()?->recordStartFromSymfonyRequest($request);

// From PHP superglobals
$flare->request()?->recordStartFromGlobals();

// From a custom provider
$flare->request()?->recordStart(new YourRequestAttributesProvider($request));

Close the span when you have a response. recordEnd() accepts optional request, response, route, and user providers; the helpers cover the common cases:

// From a Symfony / Laravel HttpFoundation response
$flare->request()?->recordEndFromSymfonyResponse($response, $request);

// From a defined status code and body size
$flare->request()?->recordEndFromDefined(statusCode: 200, bodySize: 1024);

// From custom providers
$flare->request()?->recordEnd(
    requestAttributesProvider: new YourRequestAttributesProvider($request),
    responseAttributesProvider: new YourResponseAttributesProvider($response),
);

If your framework does not produce Symfony-compatible request and response objects, implement the RequestAttributesProvider and ResponseAttributesProvider contracts. See Attribute providers for the pattern.

Routing stages

When your framework resolves a route, call the routing recorder. The five-stage breakdown (global before, routing, before, after, global after) is documented in Routing. The important call for entry-point handling is recordRoutingEnd, which fills in the flare.entry_point.handler.* attributes and triggers the sampler to re-evaluate now that the handler is known, try to resolve this as soon as possible with the least amount of wok:

$flare->routing()?->recordRoutingStart();

// ... route resolution ...

$flare->routing()?->recordRoutingEndFromDefined(
    route: '/users/{userId}',
    method: 'GET',
    handlerName: UsersController::class,
);

The handlerName is the class (or other identifier) that handles the matched route, typically the controller, closure, or method that the framework resolved. It ends up on the entry point which we discuss later.

Console commands

The command recorder produces the php_command container span and sets the entry point to cli:

// From a Symfony Console input
$flare->command()?->recordStartFromSymfonyInput($name, $input, $commandClass);

// From the raw CLI invocation (uses $_SERVER['argv'])
$flare->command()?->recordStartFromCli($name, $commandClass);

// From defined values
$flare->command()?->recordStartFromDefined($name, $arguments, $commandClass);

// ... run the command ...

$flare->command()?->recordEnd(exitCode: 0);

Queued jobs

The job recorder produces the php_job span and sets the entry point to queue. Use it inside your queue worker, around the actual job execution:

$flare->job()?->recordStartFromJob(
    jobName: 'SendWelcomeEmail',
    jobClass: SendWelcomeEmail::class,
    traceparent: $traceparent,
);

try {
    // ... process the job ...

    $flare->job()?->recordEnd();
} catch (\Throwable $exception) {
    $flare->job()?->recordFailed($exception);

    throw $exception;
}

The optional traceparent argument lets the job continue a distributed trace when the dispatching code stored a traceparent on the job payload.

For dispatching (the side that puts jobs onto a queue), use $flare->queue() to record the dispatch span. Capture the current trace's traceparent at dispatch time and store it on the job payload so the worker can continue the trace.

Queue workers and long-running processes

Long-running processes such as queue workers, Laravel Octane, RoadRunner, or a custom event loop boot once and then handle many units of work. For these processes, you do not want each job or request to start a fresh root trace. Use subtask mode instead:

  1. Configure the lifecycle with the isUsingSubtasksClosure constructor argument so it runs in subtask mode (this is wired up by your framework integration package, not by user code).
  2. Call start, register, boot once, when the worker process boots.
  3. For each unit of work, call startSubtask() and endSubtask():
while ($task = $queue->pop()) {
    $flare->lifecycle->startSubtask(traceparent: $task->traceparent);

    // Use the regular recorders inside the subtask. The job recorder
    // sets the entry point and the container span for you.
    $flare->job()?->recordStartFromJob(
        jobName: $task->name,
        jobClass: $task->class,
    );

    try {
        $task->handle();
        $flare->job()?->recordEnd();
    } catch (\Throwable $exception) {
        $flare->job()?->recordFailed($exception);
    }

    $flare->lifecycle->endSubtask();
}

Entry points

Every error, trace, and log Flare collects is attached to an entry point. The entry point describes what initiated the work, and Flare uses it both for grouping and for sampling decisions.

The default EntryPointResolver sets a base entry point as early as possible. It auto-detects whether the process is running on the web (web) or in console (cli) and fills in whatever value it can see at that point (the request URL, the command line).

That base is good enough until your framework knows more. Once an HTTP route is matched, a command class is resolved, or a queue worker picks up a job, you have two options:

  1. Overwrite the entry point. Use this when the real work is a different kind of entry point than the resolver guessed, for example a queued job picked up by a worker process that started in cli mode. Resolve the singleton and call set() with a fresh EntryPoint:

    use Spatie\FlareClient\EntryPoint\EntryPoint;
    use Spatie\FlareClient\EntryPoint\EntryPointResolver;
    use Spatie\FlareClient\Enums\EntryPointType;
    use Spatie\FlareClient\Support\Container;
    
    $resolver = Container::instance()->get(EntryPointResolver::class);
    $resolver->set(new EntryPoint(EntryPointType::Queue, $job::class));
    
  2. Extend it with a handler. Use this when the entry point itself stays correct but you can now describe what handles it (the matched route, the resolved controller, the job class). Implement EntryPointHandlerProvider alongside the matching attributes provider; the request, command, and job recorders pick it up and call setHandler() for you.

A handler is described by three attributes: handler.identifier (a groupable name like GET /users/{userId}), handler.name (the concrete class or callable, e.g. UsersController), and handler.type (a framework prefixed kind, e.g. laravel_controller). See Entry points for the full attribute schema and Attribute providers for the provider pattern.

Senders

A Sender is the transport at the bottom of the Flare client. It implements a single method, post(), which is responsible for getting an outgoing payload (an error report, a trace, or a batch of logs) to its destination. Everything above it (recorders, middleware, the Api queue) builds the payload; the sender just delivers it.

The shipped senders are:

  • CurlSender. The default. Uses libcurl to POST directly to Flare's ingress over HTTPS. Configurable through timeout and curl_options.
  • DaemonSender. Posts to a local Flare daemon over plain HTTP and lets the daemon batch and forward to Flare in the background. Falls back to a CurlSender if the daemon is unreachable. See Flare daemon.
  • GuzzleSender. A Guzzle based alternative to CurlSender, useful when your application already wires Guzzle through dependency injection.
  • NullSender. Drops every payload on the floor. Handy for tests or for environments where you want the rest of the client active without any outbound traffic.

You can read more about the daemon and its tradeoffs on the dedicated Flare daemon page.

Switching senders

Senders are configured on FlareConfig through sendUsing():

use Spatie\FlareClient\FlareConfig;
use Spatie\FlareClient\Senders\DaemonSender;

$config = FlareConfig::make('YOUR-API-KEY')
    ->useDefaults()
    ->sendUsing(DaemonSender::class, [
        'daemon_url' => 'http://127.0.0.1:8787',
    ]);

The second argument is the configuration array passed into the sender's constructor (the $config parameter). Each shipped sender documents its own keys; for example, CurlSender accepts timeout and curl_options, while DaemonSender accepts daemon_url, timeout, test_timeout, and fallback_sender_config.

Writing a custom sender

A custom sender is any class that implements Spatie\FlareClient\Senders\Sender:

namespace YourFramework\Flare\Senders;

use Closure;
use Spatie\FlareClient\Enums\FlareEntityType;
use Spatie\FlareClient\Senders\Sender;
use Spatie\FlareClient\Senders\Support\Response;

class YourSender implements Sender
{
    public function __construct(
        protected array $config = [],
    ) {
    }

    public function post(
        string $endpoint,
        string $apiToken,
        array $payload,
        FlareEntityType $type,
        bool $test,
        Closure $callback,
    ): void {
        // 1. Serialise $payload (it's a plain array).
        // 2. Send it wherever it needs to go (HTTP, queue, file, ...).
        // 3. Wrap the upstream answer in a Response and hand it to $callback.
        $callback(new Response(code: 202, body: ['ok' => true]));
    }
}

Three things to keep in mind:

  • Constructor signature. Flare's container builds the sender by calling new YourSender($config), where $config is the array you passed to sendUsing(). Accept it through constructor property promotion and pull keys from it lazily.
  • $callback is mandatory. The Api queue uses the response your sender hands to $callback to track quota, decide whether to retry, and surface delivery errors. Always call it, even on failure (with a non 2xx Response).
  • $test mode. Test payloads (sent by php artisan flare:test or Spatie\FlareClient\Support\Tester) bypass batching and expect a synchronous round trip so the caller can show the upstream response. If your transport buffers payloads, fall back to a synchronous send when $test is true.

Once written, register the custom sender the same way as any built-in sender:

$config->sendUsing(YourFramework\Flare\Senders\YourSender::class, [
    'your_option' => 'value',
]);

Testing your integration

flare-client-php ships with a Tester helper (Spatie\FlareClient\Support\Tester) and fake senders that make it straightforward to assert on the payloads your integration produces. Use them in your integration's test suite to verify that the recorders are being called with the right providers, and that the lifecycle stages are wired up in the correct order.

A reference integration to read alongside this guide is spatie/laravel-flare. Its FlareServiceProvider shows the full lifecycle wiring (including Octane subtask handling) and the request, routing, command, and job hooks in a real-world setting.

Need a hand?

Writing an integration touches a lot of moving parts. If you get stuck, run into something that isn't covered here, or want a second pair of eyes on your wiring, send us a message at [email protected]. We're happy to help.

Installation Attribute providers
  • On this page
  • Building blocks
  • Bootstrap
  • Wiring the application lifecycle
  • Recording the request
  • Routing stages
  • Console commands
  • Queued jobs
  • Queue workers and long-running processes
  • Entry points
  • Senders
  • Testing your integration
  • Need a hand?

Catch errors and fix slowdowns with Flare, the full-stack application monitoring platform for Laravel, PHP & JavaScript.

  • Platform
  • Error Tracking
  • Performance Monitoring
  • Pricing
  • Support
  • Resources
  • Insights
  • Newsletter
  • Changelog
  • Documentation
  • Affiliate program
  • uptime status badge Service status
  • Terms of use
  • DPA
  • Privacy & cookie Policy
Made in by
Flare