zerethonapp / flow-laravel
Laravel instrumentation adapter for Flow real trace capture.
Requires
- php: ^8.1
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
flow-laravel
Laravel instrumentation adapter for Flow Phase 3 & 4 (real runtime traces).
Want to see it running before installing anything?
flow-laravel-demo is a
runnable Laravel app with 7 instrumented demo routes — clone it, composer install, and hit a route to see a real captured trace.
Contributing? See CONTRIBUTING.md. Found a security issue? See SECURITY.md, not a public issue. Release history: CHANGELOG.md. This project follows the Contributor Covenant.
Why Flow?
Most developers:
- ❌ Optimize database first (without proof)
- ❌ Guess bottlenecks based on intuition
- ❌ Rely on generic monitoring tools
Flow shows the REAL bottleneck instantly:
- ✅ Identifies actual slow operations
- ✅ Prevents wasted optimization effort
- ✅ Provides clear, explainable insights
Real Example
Your request takes 100ms. Where's the bottleneck?
Without Flow:
- Maybe add database index? 🤔
- Cache everything? 🤔
- Optimize queries? 🤔
With Flow:
╔═══════════════════════════════════════════════════════════════════════════╗
║ FLOW EXECUTION ANALYSIS ║
╚═══════════════════════════════════════════════════════════════════════════╝
Top Bottleneck: UserService.fetch
Duration: 90ms
Impact: 90%
Classification: clear bottleneck
Database: 2ms (2%)
Conclusion: Database is NOT the problem. Focus on service logic instead.
What this package does
- Zero-config: Automatically captures HTTP requests after install
- Captures one real Laravel HTTP request as a Flow trace
- Produces real nodes and edges from runtime execution
- Stores trace records in
.zerethon/flow-history.json(CLI-compatible format) - Supports manual service/external instrumentation for v1 practicality
- Captures database query timings through Laravel's DB listener
Scope (v1)
Included:
- request lifecycle node
- controller scoped node
- database query nodes
- manual service traces (
Flow::trace(...)) - manual external traces (
Flow::external(...)) - JSON storage compatible with
flow-cli scan/analyze
Not included yet:
- distributed tracing
- queue/job tracing
- automatic service discovery
- UI/dashboard
Install
composer require zerethonapp/flow-laravel
That's it! Flow will now automatically trace HTTP requests in every environment except testing.
Hit any route:
curl http://your-app.test/any-route
Check the trace:
cat .zerethon/flow-history.json
Or browse individual traces:
ls storage/flow-traces/
Zero-Config Experience
By default, Flow:
- ✅ Automatically registers middleware globally
- ✅ Captures all HTTP requests (web + api)
- ✅ Captures controller execution
- ✅ Captures database queries
- ✅ Writes traces to
.zerethon/flow-history.json - ✅ Runs everywhere except
testing(production included by default — see Connected Mode below)
No code changes required.
Connected Mode (Push Traces to Flow)
By default, traces only ever land in the local .zerethon/flow-history.json
file above (Offline mode) — nothing leaves your machine/server unless you
configure Connected mode. To have every captured trace pushed automatically
to Flow, get a project's credentials from the Flow dashboard (shown once, at
project creation) and add them to .env:
FLOW_SERVER=https://flow-api.zerethon.com FLOW_PROJECT_ID=<project uuid> FLOW_SECRET_KEY=flw_sk_...
Then verify it's wired up correctly:
php artisan flow:install
That's it — no other code changes. Traces still write to the local file too (Connected mode is additive, not a replacement); if any of the three env vars are missing, Flow just skips the push silently and behaves exactly as it did before.
Production is enabled by default (except_environments only excludes
testing, see Configuration below) — the three FLOW_* env vars above are
all that's needed on a real deployed environment. If you want Flow off in a
specific environment (e.g. a staging server you don't want traced),
publish the config (php artisan vendor:publish --tag=flow-config) and add
that environment to except_environments — this check runs before
anything else in FlowServiceProvider::boot(), so an excluded environment
gets zero overhead, not just a silenced push.
Building an adapter for a different language/framework?
flow-docs/ADAPTER_PROTOCOL.md documents the exact wire contract this
package's push implements — it's plain HTTP+JSON, no PHP/Laravel-specific
mechanism involved.
Configuration (Optional)
Publish config if you want to customize behavior:
php artisan vendor:publish --tag=flow-config
The published config file at config/flow.php provides fine-grained control:
Enable/Disable
// Explicitly enable or disable (null = auto-detect based on environment) 'enabled' => env('FLOW_ENABLED'), // Environments where Flow should NOT run 'except_environments' => ['testing'], // URI patterns to exclude from tracing 'except' => [ 'telescope*', 'horizon*', ], // Sample rate (0.0 - 1.0): trace only a percentage of requests 'sample_rate' => env('FLOW_SAMPLE_RATE', 1.0),
Sources
Control which data sources are active:
'sources' => [ 'request' => true, // Request lifecycle 'controller' => true, // Controller execution 'database' => true, // Database queries 'external' => true, // External HTTP calls ],
Source Options
Configure behavior per source:
'options' => [ 'database' => [ 'capture_sql' => false, // Include SQL text in trace 'capture_bindings' => false, // Include query bindings ], ],
Storage
'storage_path' => base_path('.zerethon/flow-history.json'), 'trace_directory' => storage_path('flow-traces'), 'max_records' => 1000,
Enable request capture middleware
Add middleware alias flow.trace to routes you want to capture.
Example:
Route::middleware(['flow.trace'])->group(function () { Route::get('/orders/{id}', [OrderController::class, 'show']); });
Manual service and external tracing
Use the Facade:
use Zerethon\Flow\Laravel\Facades\Flow; $order = Flow::traceService('OrderService.findOrder', function () use ($id) { return $this->orderService->findOrder($id); }); $payload = Flow::traceExternal('BillingApi.charge', function () use ($order) { return Http::post('https://billing.example.com/charge', ['order_id' => $order->id])->json(); });
Or use the global helper:
flow()->traceService('UserService.findUser', fn () => $service->findUser($id));
Generic trace method:
Flow::trace('service', 'UserService.findUser', fn () => $service->findUser($id)); Flow::trace('external', 'PaymentApi.charge', fn () => $api->charge($amount));
Assisted Tracing
Flow provides multiple ways to make tracing easier and less verbose:
Using the Traceable Trait
Add the trait to your service classes:
use Zerethon\Flow\Laravel\Support\Traceable; class UserService { use Traceable; public function findUser(int $id): User { return $this->traceService('UserService.findUser', function () use ($id) { // Business logic here return User::find($id); }); } public function syncWithExternalApi(): void { $this->traceExternal('ExternalUserApi.sync', function () { // External API call Http::post('https://api.example.com/sync'); }); } }
Using PHP Attributes (Future)
use Zerethon\Flow\Laravel\Support\Trace; class OrderService { #[Trace('service')] public function processOrder(int $orderId): void { // Automatically traced as "OrderService.processOrder" } #[Trace('external', 'PaymentAPI.charge')] public function chargePayment(float $amount): void { // Automatically traced with custom label } }
Note: Attribute-based tracing requires additional AOP/interceptor setup and is not yet fully implemented.
Output format
Traces are appended to:
.zerethon/flow-history.json(default)
Record shape matches Flow core storage:
traceIdtimestampflow(schema_version,trace_id,nodes,edges,meta)result(status,totalTime,nodeCount,executedNodes, optionalerrors)
Analyze with existing CLI
From the Laravel app root (where .zerethon/flow-history.json exists):
flow scan flow analyze flow status
Raw JSON mode:
flow scan --json
Demo scenario (real request trace)
Use middleware + manual service/external blocks:
// routes/web.php Route::middleware(['flow.trace'])->get('/flow-demo', function () { $user = \Zerethon\Flow\Laravel\Helpers\Flow::trace('service', 'UserService.findUser', function () { return DB::table('users')->where('id', 1)->first(); }); $remote = \Zerethon\Flow\Laravel\Helpers\Flow::external('BillingApi.status', function () { return Http::get('https://httpbin.org/status/200')->status(); }); return response()->json(['user' => $user?->id, 'remote_status' => $remote]); });
Then call the route once and run:
flow scan
Config
config/flow.php
enabledstorage_pathmax_recordscapture_controllercapture_query_sql