PHPackages                             adelinferaru/nestedflowtracker - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. adelinferaru/nestedflowtracker

ActiveLibrary[HTTP &amp; Networking](/categories/http)

adelinferaru/nestedflowtracker
==============================

Trace nested application flows as timed trees. Framework-agnostic core (PSR-3/14/17/18) with first-class Laravel integration.

3.1.2(3w ago)03.1kMITPHPPHP ^8.1CI passing

Since Nov 21Pushed 3w ago1 watchersCompare

[ Source](https://github.com/adelinferaru/nestedflowtracker)[ Packagist](https://packagist.org/packages/adelinferaru/nestedflowtracker)[ Docs](https://adelinferaru.github.io/nestedflowtracker/)[ RSS](/packages/adelinferaru-nestedflowtracker/feed)WikiDiscussions master Synced 3w ago

READMEChangelog (10)Dependencies (16)Versions (15)Used By (0)

NestedFlowTracker
=================

[](#nestedflowtracker)

[![CI](https://github.com/adelinferaru/nestedflowtracker/actions/workflows/ci.yml/badge.svg)](https://github.com/adelinferaru/nestedflowtracker/actions/workflows/ci.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/5ee517618f2f5ab2bbe9e250cf814d1985dd661698a912fc6beba7d7c75bec0f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6164656c696e6665726172752f6e6573746564666c6f77747261636b65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/adelinferaru/nestedflowtracker)[![Total Downloads](https://camo.githubusercontent.com/6a65e7a560793c1dc70e568c49e82d6276d26d1470353bd93b8ad9854f2fd901/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6164656c696e6665726172752f6e6573746564666c6f77747261636b65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/adelinferaru/nestedflowtracker)[![PHP Version](https://camo.githubusercontent.com/87dc45e1a87fe7706168500bea06210a04dc11a086913a5f3e73bed3845e55cb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6164656c696e6665726172752f6e6573746564666c6f77747261636b65722f7068702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/adelinferaru/nestedflowtracker)[![License](https://camo.githubusercontent.com/06a69836f7d4eb7cf45ae786f53e74d88d4fec622c8464f3471e3d1ec28fa6ab/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6164656c696e6665726172752f6e6573746564666c6f77747261636b65722e7376673f7374796c653d666c61742d737175617265)](license.md)

**[adelinferaru.github.io/nestedflowtracker](https://adelinferaru.github.io/nestedflowtracker/)**

A **zero-infra flow tracer**. Wrap any block of code in a *span*; it gets timed and stored as a tree in your own database, with nested sub-operations recorded as children. A single flow can span multiple applications via a shared `trace_id`.

No collectors, no external backend — unlike OpenTelemetry you need no infrastructure, and unlike Telescope it traces *your* business flows (not framework internals) and works in production.

[![Wrap a checkout in spans, then watch the built-in viewer render it as a timed tree](art/demo.gif)](art/demo.gif)

**Requires** PHP 8.1+. As of **3.0** the package is split into a framework-agnostic **Core** (only PSR-3/14/17/18 dependencies) and a **Laravel** adapter (auto-discovered on Laravel 10, 11, 12, or 13; L13 needs PHP 8.3+). Use either side independently.

Installation
------------

[](#installation)

```
composer require adelinferaru/nestedflowtracker
```

Publish and run the migration:

```
php artisan vendor:publish --tag="flow-migrations"
php artisan migrate
```

Optionally publish the config:

```
php artisan vendor:publish --tag="flow-config"
```

### Upgrading from 2.x

[](#upgrading-from-2x)

3.0 is namespace-only: behaviour, config keys, env vars, the `flow_spans` schema and the artisan commands are all unchanged. `composer update` plus a search-and-replace on imports usually does it. The full namespace table lives in [`changelog.md`](changelog.md). The most common moves:

- `AdelinFeraru\NestedFlowTracker\Facades\Flow` → `…\Laravel\Facades\Flow`
- `AdelinFeraru\NestedFlowTracker\Models\FlowSpan` → `…\Laravel\Eloquent\FlowSpan`
- `AdelinFeraru\NestedFlowTracker\Events\SpanFinished` → `…\Core\Events\SpanFinished`
- `AdelinFeraru\NestedFlowTracker\TraceContext` → `…\Core\TraceContext`

Usage
-----

[](#usage)

The recommended API is `span()`: it opens a span, runs your callback, and closes it automatically — even if the callback throws. It returns the callback's value untouched.

```
use AdelinFeraru\NestedFlowTracker\Laravel\Facades\Flow;

$account = Flow::span('register user', function () use ($data) {
    $account = Flow::span('create account', fn () => Account::create($data));

    Flow::span('send welcome email', fn () => Mail::to($account)->send(new Welcome()));

    return $account;
});
```

This records a tree:

```
register user .................. 142ms
├─ create account .............. 38ms
└─ send welcome email .......... 95ms

```

You can also use the `flow()` helper or resolve the service from the container:

```
flow()->span('charge card', fn () => $gateway->charge($card));

app(\AdelinFeraru\NestedFlowTracker\Core\FlowTracker::class)->span(/* ... */);
```

### Without Laravel

[](#without-laravel)

Construct `Core\FlowTracker` yourself and drive it directly. Any PSR-3 logger / PSR-14 dispatcher work; the package ships PDO-backed storage drivers, a PSR-18 OTLP exporter, and a `null` driver.

```
use AdelinFeraru\NestedFlowTracker\Core\Drivers\PdoDriver;
use AdelinFeraru\NestedFlowTracker\Core\Drivers\PdoSchema;
use AdelinFeraru\NestedFlowTracker\Core\FlowConfig;
use AdelinFeraru\NestedFlowTracker\Core\FlowTracker;

$pdo = new PDO('sqlite:flows.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
PdoSchema::create($pdo);                                    // sqlite / mysql / pgsql

$flow = new FlowTracker(
    new FlowConfig(enabled: true, component: 'orders'),
    $events,                                                // any PSR-14 EventDispatcherInterface
    new PdoDriver($pdo),                                    // or BufferedPdoDriver for one bulk insert per flow
);

$flow->span('checkout', function ($span) use ($flow) {
    $flow->span('charge card', fn () => /* ... */);
});
```

Other Core drivers: `LogDriver(LoggerInterface)` (PSR-3), `NullDriver`, and `OtelDriver` (wraps the PSR-18/17 `Core\Otel\OtelExporter`). All implement `Core\Drivers\SpanDriver` — bring your own if you want a different backend.

A complete runnable round trip — trace a flow (including a failed span), store it in SQLite, read the tree back with plain SQL — lives in [`examples/plain-php.php`](examples/plain-php.php):

```
php examples/plain-php.php
```

```
checkout                        66.1 ms
   charge card                     43.1 ms
   reserve stock                   12.6 ms
   send confirmation email         10.0 ms  ⚠ failed

```

### Enriching a span

[](#enriching-a-span)

The open span is passed to your callback:

```
Flow::span('import csv', function ($span) use ($rows) {
    $span->context = ['rows' => count($rows)];
    $imported = $this->import($rows);
    $span->result = ['imported' => $imported];
    return $imported;
});
```

### Manual spans

[](#manual-spans)

When you cannot wrap the work in a closure, open and close spans manually (LIFO — the innermost open span is closed first):

```
Flow::start('long running process');
// ...
Flow::end(['result' => ['ok' => true]]);
```

### Across applications (W3C Trace Context)

[](#across-applications-w3c-trace-context)

Flows propagate across services via the standard [`traceparent`](https://www.w3.org/TR/trace-context/)header (our `trace_id` is already a 32-hex W3C trace id).

**Outbound** — add the current trace to an HTTP client call:

```
Http::withFlowTrace()->post('https://orders.internal/checkout', $payload);
```

**Inbound** — with `flow.auto.http` enabled, an incoming `traceparent` is read automatically and the request's root span continues the upstream trace. Doing it manually:

```
use AdelinFeraru\NestedFlowTracker\Core\TraceContext;

if ($ctx = TraceContext::parse($request->header('traceparent'))) {
    Flow::setTraceId($ctx->traceId);
}
```

Artisan commands
----------------

[](#artisan-commands)

```
php artisan flow:show {trace}   # print a flow as a tree
php artisan flow:prune --days=30 # delete flow spans older than N days
```

### Events

[](#events)

`SpanStarted` and `SpanFinished` are dispatched as spans open and close, so you can react to them (e.g. log slow spans):

```
use AdelinFeraru\NestedFlowTracker\Core\Events\SpanFinished;

Event::listen(function (SpanFinished $event) {
    if ($event->span->duration > 1.0) {
        Log::warning("Slow span: {$event->span->name} ({$event->span->duration}s)");
    }
});
```

### Automatic instrumentation

[](#automatic-instrumentation)

Opt in to record spans with **zero manual calls**:

```
FLOW_AUTO_HTTP=true    # a root span per HTTP request (web + api groups)
FLOW_AUTO_QUEUE=true   # a root span per queued job
```

- **HTTP:** every request gets a root span named like `GET users/{id}`, with the method, path and response status in its context; it's marked `failed` on a 5xx response or an exception. Any manual `Flow::span()` calls during the request automatically nest underneath it.
- **Queue:** every processed job gets a root span (`job: App\Jobs\...`); failed jobs are recorded as `failed`. Each job is an isolated trace.

Both default to off, so installing the package never silently writes spans.

### The `#[Trace]` attribute

[](#the-trace-attribute)

Annotate a route action (or a whole controller) or a queued job to wrap it in a span — the attribute is the opt-in, no other code or config:

```
use AdelinFeraru\NestedFlowTracker\Core\Attributes\Trace;

class CheckoutController
{
    #[Trace('checkout')]                  // or #[Trace] to name it after the action
    public function store(Request $request) { /* … */ }
}

#[Trace]                                  // class-level: on a job (or every controller action)
class SendInvoice implements ShouldQueue { /* … */ }
```

- **Route actions** are wrapped via middleware on the `web`/`api` groups; with `FLOW_AUTO_HTTP`on, the action span nests under the request's root span. 5xx responses mark the span failed.
- **Queued jobs** with the attribute are traced even when `FLOW_AUTO_QUEUE` is off — per-job opt-in instead of all-jobs. Failed jobs record the exception.
- PHP attributes are metadata, not decorators — there's no engine-level interception, so the attribute works where the package owns the call site (actions and jobs). For arbitrary service methods, wrap with `Flow::span()`.
- Detection costs one cached reflection lookup per request/job. Kill switch: `FLOW_ATTRIBUTES=false`.

Viewer
------

[](#viewer)

A small built-in UI to browse recorded flows as timed trees — no build step, no assets to compile. Enable it and visit `/flow`:

```
FLOW_VIEWER=true
```

- **Index** (`/flow`) — recent flows with their component, status and duration; filter by component/status.
- **Detail** (`/flow/{trace}`) — the flow rendered as a collapsible tree with duration bars and failed spans highlighted.

[![The viewer index listing recent flows](art/index.png)](art/index.png)

**Access control:** the viewer is reachable automatically in the `local` environment. In any other environment you must define a `viewFlow` gate to grant access:

```
use Illuminate\Support\Facades\Gate;

Gate::define('viewFlow', fn ($user) => $user->isAdmin());
```

Publish the views to customize them: `php artisan vendor:publish --tag="flow-views"`.

### JSON API

[](#json-api)

The viewer also exposes a read API (same enable flag + `viewFlow` gate):

```
GET {path}/api/flows               # recent flows; ?component=, ?status=, ?per_page=, ?page=
GET {path}/api/flows/{trace}       # one flow as a nested span tree

```

```
// GET /flow/api/flows/{trace}
{ "trace_id": "…", "spans": [ { "name": "checkout", "status": "ok", "duration": 0.19,
  "children": [ { "name": "charge card", "status": "ok", "duration": 0.08, "children": [] } ] } ] }
```

For token-based/stateless API clients, set `flow.viewer.middleware` to `['api']`.

Storage drivers
---------------

[](#storage-drivers)

Choose where finished spans go with `flow.driver`:

DriverStores spans asViewer / `flow:*``database` (default)a tree in your database✅`log`structured log lines (`flow.log.channel`)—`null`discarded (API stays on)—`otel`sent straight to an OTLP collector, no DB—```
FLOW_DRIVER=database   # database | log | null | otel
```

The viewer, the artisan commands, and the `flow.otel` export below are **database-only** features (they read from the `flow_spans` table). The `log`, `null`, and `otel` drivers are emit-only.

OpenTelemetry export
--------------------

[](#opentelemetry-export)

Already running an OpenTelemetry Collector, Jaeger, or Grafana Tempo? Ship completed flows there too — no OTel SDK required, we just POST OTLP-JSON. When a flow's root span closes, the whole trace is exported on a queue.

```
FLOW_OTEL_ENABLED=true
FLOW_OTEL_ENDPOINT=http://localhost:4318   # spans are sent to {endpoint}/v1/traces
```

This is the database path: spans are stored *and* exported. If you don't want to store them at all, use the `otel` storage driver above (`FLOW_DRIVER=otel`), which sends spans straight to the collector with no database.

> Upgrading from an earlier 2.x? Re-publish and run migrations after upgrading: `php artisan vendor:publish --tag="flow-migrations" && php artisan migrate`. Run a queue worker so exports happen off the request.

Configuration
-------------

[](#configuration)

EnvConfig keyDefaultDescription`FLOW_ENABLED``flow.enabled``true`Master switch. When off, `span()` runs your callback transparently and stores nothing.`FLOW_COMPONENT``flow.component``app`Name of this application/service, stored on every span.`FLOW_DRIVER``flow.driver``database`Storage driver: `database` / `log` / `null` / `otel`.`FLOW_BUFFER``flow.buffer``false`Buffer a flow and bulk-insert on completion (database driver).`FLOW_LOG_CHANNEL``flow.log.channel``null`Log channel for the `log` driver (null = default).`FLOW_CONNECTION``flow.connection``null`Connection for the `flow_spans` table (null = default).`FLOW_AUTO_HTTP``flow.auto.http``false`Auto root span per HTTP request.`FLOW_AUTO_QUEUE``flow.auto.queue``false`Auto root span per queued job.`FLOW_ATTRIBUTES``flow.attributes``true`Honor the `#[Trace]` attribute on actions/jobs.`FLOW_VIEWER``flow.viewer.enabled``false`Register the built-in viewer routes.`FLOW_VIEWER_PATH``flow.viewer.path``flow`URL prefix for the viewer.`FLOW_OTEL_ENABLED``flow.otel.enabled``false`Export completed flows to an OTLP/HTTP collector.`FLOW_OTEL_ENDPOINT``flow.otel.endpoint``null`Collector base URL (spans go to `{endpoint}/v1/traces`).Performance
-----------

[](#performance)

Tracking costs nothing when off and little when on — measure it for your setup:

```
php artisan flow:benchmark --flows=300 --spans=5
```

Indicative per-span overhead (300 flows × 6 spans, in-memory SQLite — **your database and hardware will differ**, the `database` figure especially):

Scenarioµs / spandisabled (`flow.enabled=false`)~2`null` driver (tracking, no storage)~60`database` driver (immediate)~1030`database` driver (`flow.buffer=true`)~125The immediate `database` cost is dominated by the two writes per span. **Buffered mode**(`FLOW_BUFFER=true`) holds a whole flow in memory and bulk-inserts it in a single query when the flow completes — roughly **8× faster** here. The trade-off: spans are only persisted once the flow completes (a crash mid-flow loses it), so it's off by default. `flow_spans` is indexed on `trace_id`, `span_id`, `component`, `status`, and `created_at`.

Testing
-------

[](#testing)

```
composer test
composer analyse
```

Credits
-------

[](#credits)

- [Feraru Ioan Adelin](https://github.com/adelinferaru)
- [All Contributors](../../contributors)

License
-------

[](#license)

MIT. Please see the [license file](license.md) for more information.

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance95

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity74

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~184 days

Recently: every ~3 days

Total

14

Last Release

23d ago

Major Versions

1.0 → v2.x-dev2020-04-04

2.5.1 → 3.0.02026-06-08

PHP version history (2 changes)v2.x-devPHP &gt;=7.1.3

2.0.0PHP ^8.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4851269?v=4)[Feraru Ioan Adelin](/maintainers/adelinferaru)[@adelinferaru](https://github.com/adelinferaru)

---

Top Contributors

[![adelinferaru](https://avatars.githubusercontent.com/u/4851269?v=4)](https://github.com/adelinferaru "adelinferaru (37 commits)")

---

Tags

laravellaravel-packageobservabilityopentelemetryperformance-monitoringphpspanstracinglaravelpsr-18tracingopentelemetryflowspans

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

### Embed Badge

![Health badge](/badges/adelinferaru-nestedflowtracker/health.svg)

```
[![Health](https://phpackages.com/badges/adelinferaru-nestedflowtracker/health.svg)](https://phpackages.com/packages/adelinferaru-nestedflowtracker)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M421](/packages/drupal-core-recommended)[symfony/symfony

The Symfony PHP framework

31.4k87.2M2.2k](/packages/symfony-symfony)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M574](/packages/shopware-core)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
