lookout / tracing
Distributed tracing, sampled performance spans (limits, Laravel collectors), optional browser RUM beacons, breadcrumbs, structured logs and custom metrics ingest, exception reporting with enrichment pipeline, cron check-ins, and profiling ingest for Lookout.
Requires
- php: ^8.3
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- laravel/framework: ^13.0
- monolog/monolog: ^3.0
- phpunit/phpunit: ^12.5
- psr/http-client: ^1.0
- symfony/http-client: ^7.0|^8.0
Suggests
- guzzlehttp/guzzle: Use Lookout\Tracing\Http\GuzzleTraceMiddleware or Psr18TraceClient for outgoing headers and spans.
- laravel/framework: Use Lookout\Tracing\Laravel\LookoutTracingServiceProvider and middleware.
- monolog/monolog: Use Lookout\Tracing\Logging\Monolog\LookoutMonologHandler to forward Monolog records to Lookout log ingest.
- psr/http-client: Use Lookout\Tracing\Http\Psr18TraceClient to wrap any PSR-18 HTTP client.
- slim/slim: Wire Lookout\Tracing\Http\ContinueTracePsr15Middleware in a PSR-15 stack (also works with Mezzio and similar).
- symfony/http-client: Use Lookout\Tracing\Http\SymfonyHttpClientTraceDecorator to wrap Symfony HttpClientInterface.
- symfony/var-dumper: Required for dump() breadcrumbs when LOOKOUT_INSTRUMENT_DUMP is enabled (Laravel already includes it).
This package is auto-updated.
Last update: 2026-03-27 19:30:06 UTC
README
PHP library for Lookout distributed tracing with Sentry-compatible headers and manual instrumentation. You do not need the Sentry SDK; APIs mirror Sentry PHP tracing instrumentation and trace propagation so existing patterns transfer easily.
Install
composer require lookout/tracing
(This repository vendors the package from packages/lookout-tracing via a Composer path repository.)
Propagation
- Incoming: parse
sentry-traceandbaggage(e.g. fromPSR-7request headers or Laravel’sRequest). - Outgoing: add the same headers to downstream HTTP calls so other services can continue the trace.
use Lookout\Tracing\Tracer; Tracer::instance()->continueTrace( $request->getHeaderLine('sentry-trace'), $request->getHeaderLine('baggage'), ); $headers = Tracer::instance()->outgoingTraceHeaders(); // [ 'sentry-trace' => '...', 'baggage' => '...' ]
HTML meta tags for browser SDKs:
use Lookout\Tracing\HtmlTraceMeta; echo HtmlTraceMeta::render();
Custom instrumentation
use Lookout\Tracing\SpanOperation; use Lookout\Tracing\Tracing; $tx = Tracing::startTransaction('GET /orders', SpanOperation::HTTP_SERVER); Tracing::trace(function () { // … }, SpanOperation::HTTP_CLIENT, 'GET https://api.example.com/v1/orders'); $tx->finish();
Common op values are defined on Lookout\Tracing\SpanOperation (http.server, http.client, db.query, cache.get, queue.publish, etc.).
Lookout ingest
Tracer::errorIngestTraceFields()—trace_id,span_id,parent_span_id,transactionfor your error JSON body toPOST /api/ingest.Tracer::errorIngestPerformanceGroupingHints()— whenreporting.performance_grouping.enabledis true (envLOOKOUT_REPORT_PERFORMANCE_GROUPING) andperformance_enabledrecorded spans in the same request, may addgrouping_slow_pathandgrouping_db_time_msso Lookout can fingerprint slow / DB-heavy errors separately (see Lookout ingest docs).Tracer::configure([...])+Tracer::flush()— send finished spans toPOST /api/ingest/trace(setapi_key,base_uri, optionalenvironment/release). UseTracer::flushWithResult()(orTracing::flushWithResult()) when you need the HTTP status (e.g. 403 if the Lookout project disabled trace ingest).
Structured logs (Sentry-style)
Similar in spirit to Sentry PHP logs: lookout_logger()->info('User %s logged in', ['alice']), optional flush(), and a Monolog handler. Rows go to POST /api/ingest/log with the same api_key / base_uri as tracing; enable with LOOKOUT_LOGS_ENABLED=true (Laravel: config/lookout-tracing.php → logging.enabled). Laravel registers a terminating flush when logging.enabled and logging.flush_on_terminate are true. Long workers should call lookout_logger()->flush() on a timer or after batches.
lookout_logger()->info('order placed', null, ['order_id' => '42']); lookout_logger()->flush();
use Lookout\Tracing\Logging\Monolog\LookoutMonologHandler; use Monolog\Logger; $log = new Logger('app'); $log->pushHandler(new LookoutMonologHandler());
Custom metrics (Sentry-style)
Similar in spirit to Sentry PHP metrics: lookout_metrics()->count('orders.completed', 1), gauge(), distribution(), optional MetricUnit, and flush(). Samples go to POST /api/ingest/metric; the active trace_id is attached when a transaction is in flight so the Lookout UI can correlate rollups with traces. Enable with LOOKOUT_METRICS_ENABLED=true (Laravel: metrics.enabled). Laravel flushes on terminating when metrics.enabled and metrics.flush_on_terminate are true.
Optional MetricsIngestClient::configure(['before_send_metric' => fn (array $row): ?array => $row]) drops or mutates rows before enqueue (return null to skip), like Sentry’s before_send_metric.
use Lookout\Tracing\Metrics\MetricUnit; lookout_metrics()->count('button.click', 5, ['plan' => 'pro']); lookout_metrics()->distribution('page.load_ms', 42.5, ['route' => '/checkout'], MetricUnit::millisecond()); lookout_metrics()->flush();
Real User Monitoring (browser)
Optional Web Vitals + SPA / Livewire navigation beacons: POST /api/ingest/rum (same project API key; performance ingest must be enabled on the project). Vanilla script with no npm dependencies:
resources/rum/lookout-rum.js—LookoutRum.init({ endpoint, apiKey, livewireNavigate: true, traceId: () => … }). Putsapi_keyin the JSON body sonavigator.sendBeaconworks without custom headers. Correlate with server traces viatrace_id(32 hex), e.g. fromHtmlTraceMeta/ a<meta name="lookout-trace-id">you render fromTracer::instance()->traceIdon the server.
Error reporting client
Uncaught exceptions use Lookout\Tracing\Reporting\ErrorReportClient: middleware enriches the payload (Laravel + HTTP context, git metadata, context.attributes from Lookout\Tracing\Reporting\ReportScope and configurable AttributeProviderInterface classes, optional client_solutions strings), then ReportTruncator enforces Lookout size limits, optional ReportSampler drops a random fraction, and the payload is POSTed immediately or queued and flushed on shutdown (reporting.queue / reporting.send_immediately).
Glows (Flare-style manual breadcrumbs)
Similar in spirit to Flare Laravel glows: custom timeline notes that appear with other breadcrumbs on the error in Lookout (chronological “what ran before this failed”).
use Lookout\Tracing\GlowBreadcrumb; GlowBreadcrumb::glow('Payment branch: validated wallet', 'info', ['wallet_id' => $id]); GlowBreadcrumb::glow('Skipping cache (feature flag)', 'debug');
$message— required; trimmed, max length enforced with other breadcrumbs.$level— string such asdebug,info,warning,error(defaultinfo).$data— optional associative array (subject to the same redaction as other breadcrumb payloads).
Internally these are breadcrumbs with type glow and category glow. They are not the Spatie Flare::glow() API—there is no drop-in facade. They attach to the error ingest breadcrumb list, not as separate span events on traces (Flare also shows glows on spans in performance; Lookout’s buffer is scoped to the next error report).
Manual filesystem breadcrumbs
For disk I/O there is no universal Laravel hook; use FilesystemBreadcrumb::record():
use Lookout\Tracing\FilesystemBreadcrumb; FilesystemBreadcrumb::record('read', '/var/app/config.json', 'info', ['bytes' => 1024]);
Optional breadcrumb recorders (same config block as core instrumentation, instrumentation.enabled must be true): cache hits/misses, Redis commands, views (view composer *), outbound HTTP (Illuminate\Http\Client events), response metadata (ResponsePrepared), database transactions (TransactionBeginning / Committed / RolledBack), dump() via Symfony VarDumper, plus manual Lookout\Tracing\GlowBreadcrumb::glow() and Lookout\Tracing\FilesystemBreadcrumb::record(). Env flags: LOOKOUT_INSTRUMENT_CACHE, _REDIS, _VIEWS, _OUTBOUND_HTTP, _RESPONSE_DETAIL, _DATABASE_TRANSACTIONS, _DUMP. Set LOOKOUT_INSTRUMENT_COMPREHENSIVE_COLLECTION=true to turn on the optional recorders above (plus SQL breadcrumbs and performance collectors for cache, Redis, views, log) in one step.
Broad Laravel error context (what maps where)
| Area | Lookout |
|---|---|
| Application info | context.laravel: framework + PHP version, application name, locale, config cached, debug, application_env (APP_ENV), route/command/queue hints |
| Laravel context | Same context.laravel + context.log_context from context() / Illuminate\Log\Context\Repository |
| Exception context | context.exception_context when the throwable implements context() (redacted) |
| Stacktrace arguments | Structured stack_frames[].args when reporting.include_stack_arguments is true and PHP supplies trace args (zend.exception_ignore_args=0) |
| Requests / URL / user | url, user, issue_route, context.server; HTTP breadcrumbs |
| Server info | context.server (hostname, SAPI, OS, pid, limits, tz) + request SERVER_ADDR when present |
| Git information | Default GitInformationMiddleware (commit, etc.) |
| Solutions | SolutionsMiddleware + reporting.client_solutions |
| Console commands | Breadcrumbs + performance spans when enabled |
| Jobs and queues | Breadcrumbs + queue trace propagation + performance |
| Queries | Optional SQL breadcrumbs; DB spans + query insights when performance DB collector on |
| Database transactions | Breadcrumbs when instrumentation.database_transactions or comprehensive_collection |
| Cache events | Breadcrumbs + optional cache spans |
| Redis commands | Breadcrumbs + optional Redis spans |
| External HTTP | Breadcrumbs + http.client spans (Guzzle / Http::) |
| Views | View composer breadcrumbs + optional view spans |
| Logs | Optional MessageLogged breadcrumbs; optional log spans; structured /api/ingest/log via lookout_logger() |
| Livewire | context.livewire (component class + name) on Livewire requests |
| Spans / errors when tracing | LOOKOUT_PERFORMANCE_ENABLED, Tracer::markTraceMustExport on error reports |
| Dumps | instrumentation.dump → DumpInstrumentation |
| Glows / filesystem | Manual GlowBreadcrumb::glow(), FilesystemBreadcrumb::record() |
| Customise report | reporting.middleware, AttributeProviderInterface, ReportScope |
Global no-op: LOOKOUT_DISABLED or reporting.disabled. Ingest fields is_log, open_frame_index, and grouping_override (custom fingerprint when fingerprint is empty; camelCase aliases isLog, openFrameIndex, overriddenGrouping) are stored on the server. In the Lookout app, Project → Monitoring modes can turn off POST /api/ingest/trace and POST /api/ingest/rum per project while leaving error ingest enabled.
User feedback (crash page)
Similar in spirit to Sentry user feedback: when ErrorReportClient builds an error payload it ensures an occurrence_uuid (v4) and remembers it for lookout_last_error_occurrence_uuid() / ErrorReportClient::lastOccurrenceUuid(). On your custom error view, POST that UUID with the user’s message to POST /api/ingest/feedback (same project api_key; see Lookout Ingest API → User feedback). The comment appears on that occurrence’s thread in the app. Alternatively use the ingest response / read API event_id (ULID) as event_id in the feedback body.
Cron monitors (Sentry Crons–style)
Aligned with Sentry PHP Crons: in_progress → ok / error, optional heartbeat, and monitor upsert via monitor_config.
use Lookout\Tracing\Cron\CheckInStatus; use Lookout\Tracing\Cron\Client as CronClient; use Lookout\Tracing\Cron\MonitorConfig; use Lookout\Tracing\Cron\MonitorSchedule; CronClient::configure([ 'api_key' => getenv('LOOKOUT_API_KEY'), 'base_uri' => 'https://your-lookout-host.example', 'cron_ingest_path' => '/api/ingest/cron', ]); $config = MonitorConfig::make(MonitorSchedule::crontab('0 * * * *'), checkinMarginMinutes: 5); $id = CronClient::captureCheckIn('hourly-job', CheckInStatus::inProgress(), monitorConfig: $config); CronClient::captureCheckIn('hourly-job', CheckInStatus::ok(), $id); CronClient::withMonitor('wrapped-job', fn () => doWork(), $config); CronClient::captureCheckIn('heartbeat', CheckInStatus::ok(), null, 12.0);
Optional meta (string/number/bool values, size-limited server-side) on captureCheckIn attaches context to the check-in row and merges on completion.
Laravel: the same service provider configures CronClient from config/lookout-tracing.php (cron_ingest_path defaults to /api/ingest/cron).
Profiling (CPU / flame graphs)
Aligned with Sentry PHP profiling in spirit: capture with Excimer (speedscope JSON), xhprof / Tideways, SPX, or cooperative php.manual_pulse sampling (no extension), then POST to Lookout.
use Lookout\Tracing\Profiling\ProfileClient; ProfileClient::configure([ 'api_key' => getenv('LOOKOUT_API_KEY'), 'base_uri' => 'https://your-lookout-host.example', 'profile_ingest_path' => '/api/ingest/profile', ]); ProfileClient::sendProfile([ 'agent' => 'other', 'format' => 'speedscope', 'data' => [/* speedscope JSON object */], 'trace_id' => 'abc123…', 'transaction' => 'GET /checkout', ]);
First-party aggregate hotspots (lookout.v1):
use Lookout\Tracing\Profiling\LookoutProfileV1Payload; use Lookout\Tracing\Profiling\ProfileClient; ProfileClient::sendProfile(LookoutProfileV1Payload::aggregateIngestBody( [ ['file' => 'app/Services/Checkout.php', 'line' => 120, 'samples' => 48], ], meta: ['source' => 'custom-collector'], context: ['trace_id' => 'abc123…', 'transaction' => 'POST /checkout'], ));
Package classes under Lookout\Tracing\Profiling\ (e.g. ExcimerExporter, XhprofLikeExporter, SpxPayload, ManualPulseSampler, LookoutProfileV1Payload) help build agent / format / data for each backend. Laravel: LookoutTracingServiceProvider merges the same api_key, base_uri, and profile_ingest_path from config/lookout-tracing.php.
Overhead: Lookout does not sample profiles for you — wrap ProfileClient::sendProfile() (or your Excimer/Tideways hooks) so production only uploads a small fraction of requests or when duration exceeds a threshold, similar to profiles_sample_rate / slow-transaction rules elsewhere.
Laravel
Auto-discovery registers Lookout\Tracing\Laravel\LookoutTracingServiceProvider.
- Middleware alias:
lookoutTracing.continueTrace— callcontinueTrace()from incoming headers. - Publish config:
php artisan vendor:publish --tag=lookout-tracing-config - Env:
LOOKOUT_API_KEY,LOOKOUT_BASE_URI(orAPP_URL), optionalLOOKOUT_TRACING_AUTO_FLUSH=true. Profile ingest path defaults to/api/ingest/profile(override in published config).
Framework breadcrumbs & exception reporting
The provider registers event listeners (when instrumentation.enabled is true) that append breadcrumbs for:
| Area | Laravel events (indicative) |
|---|---|
| HTTP | RouteMatched, RequestHandled |
| Console | CommandStarting, CommandFinished |
| Queue | JobProcessing, JobProcessed, JobFailed, JobExceptionOccurred |
| Optional | QueryExecuted (sampled), MessageLogged, allowlisted domain events, or a wildcard listener |
Breadcrumbs are cleared at each route match, Artisan command, or queue job so queue:work and Octane do not mix unrelated requests.
Set LOOKOUT_REPORT_EXCEPTIONS=true (plus API key and base URI) to register a reportable handler on the default exception handler. It POSTs to POST /api/ingest with:
- exception message, class, stack trace, and stack frames
- current breadcrumbs
- trace fields from
Tracer::errorIngestTraceFields()when a transaction was started context.laravel: framework version, PHP version, route, queue job name, Artisan command, HTTP path/method when available
Tune knobs in config/lookout-tracing.php (instrumentation.*, breadcrumbs_max, error_ingest_path).
Performance monitoring (traces & spans)
Enable with LOOKOUT_PERFORMANCE_ENABLED=true (and keep LOOKOUT_API_KEY / LOOKOUT_BASE_URI set). This turns on sampled span recording: OpenTelemetry-style trace ids, spans, and optional span events, sent to POST /api/ingest/trace via Tracer::flush() or LOOKOUT_TRACING_AUTO_FLUSH=true. Ensure the project allows trace ingest in Lookout → Project settings → Monitoring modes; otherwise the API returns 403.
- Middleware (order matters): register
lookoutTracing.continueTracefirst, thenlookoutTracing.performance, or setLOOKOUT_PERFORMANCE_AUTO_MIDDLEWARE=trueto append only the performance middleware towebandapi(you still addcontinueTraceyourself if it is not already in those groups). - Sampling: default
RateSamplerat 10% (LOOKOUT_PERFORMANCE_SAMPLE_RATE=0.1). ImplementLookout\Tracing\Performance\Samplerand setperformance.sampler.classfor custom logic. Traces continued viasentry-tracewithsampled=0never record spans (propagation only). Optional tail sampling (LOOKOUT_PERFORMANCE_TAIL_SAMPLING=true): keep slow roots (LOOKOUT_PERFORMANCE_TAIL_SLOW_MS), errors / 5xx, optionalLOOKOUT_PERFORMANCE_TAIL_RESIDUAL_RATEfor a thin random sample of the rest — same theme as loweringtraces_sample_ratein production while still capturing outliers. - Limits:
performance.trace_limits— max spans per export, max attributes per span / span event, max span events per span. - Hooks:
Tracing::configureSpans(fn (Span $span) => …)andTracing::configureSpanEvents(fn (array $event) => …|null)— returnnullfrom the span-event callback to drop an event. - Collectors (
performance.collectors.*): HTTP server transaction, database queries (childdb.queryspans), console / queue root transactions, log lines as span events, and HTTP client spans when you attachGuzzleTraceMiddleware(see below).
CLI / queue: enable LOOKOUT_PERFORMANCE_FLUSH_CLI_QUEUE=true to flush after each command or job, or call Tracing::flush() yourself.
Rails
For Ruby on Rails, use the copy-paste module under packages/lookout-rails/ in the Lookout repository (lib/lookout_framework.rb + README), or a git subtree mirror if you use SPLIT_LOOKOUT_RAILS_REPO: ActiveSupport::Notifications for controller and Active Job, optional SQL sampling, and LookoutFramework.report_exception from your error pipeline.
Guzzle 7
use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use Lookout\Tracing\Http\GuzzleTraceMiddleware; $stack = HandlerStack::create(); $stack->push(GuzzleTraceMiddleware::create()); $client = new Client(['handler' => $stack]);
With performance monitoring enabled, the same middleware also records http.client child spans (when a parent span is active and sampling allows recording).
Requirements
- PHP 8.3+
psr/http-message(for the optional Guzzle middleware type hints)guzzlehttp/guzzle(optional, forGuzzleTraceMiddleware+ promises)
SDK roadmap & Lookout alignment
The Lookout app surfaces Traces, Transactions, and trace detail in the web UI; the SDK sends errors (POST /api/ingest) and, when enabled, spans (POST /api/ingest/trace) with consistent trace_id / sentry-trace propagation.
| Server behavior | SDK support |
|---|---|
performance_ingest_enabled false |
Trace ingest returns 403. Laravel: enable performance.sync_from_api (Sanctum token + project id) so Tracer::isPerformanceEnabled() matches the server on boot; or set LOOKOUT_PERFORMANCE_ENABLED=false. Auto-flush and queue/cli flush log lookout.tracing.trace_forbidden when performance.log_forbidden_trace_ingest is true (default). |
GET /api/v1/projects/{id} |
LookoutManagementApi::fetchProject() + sync config (see lookout-tracing.php performance.sync_from_api). |
| 429 / flaky network | trace_ingest.max_attempts, retry_delay_ms, retry_statuses (env: LOOKOUT_TRACE_INGEST_*) — Tracer::flushWithResult() uses HttpTransport::postJsonWithResponseRetries(). 403 is never retried. |
Implemented building blocks
Lookout\Tracing\Interop\OpenTelemetryTraceConverter— OTLP JSON → Lookout:toJobPayloads()(one row set pertraceId),toLookoutIngestBody()when only one trace is present,fromLookoutIngestBody()for OTLP export from native bodies. Lookout HTTP:POST /api/ingest/trace/otlp(same auth/gate as/api/ingest/trace).Lookout\Tracing\Http\ContinueTracePsr15Middleware— PSR-15sentry-trace/baggageparsing (Slim, Mezzio, etc.).Lookout\Tracing\Support\DataRedactor::redact()— recursive redaction for spandata/ context-style arrays.Lookout\Tracing\Testing\TracerInspection::traceIngestBody()— stable access tobuildTraceIngestBody()in tests.
Still optional / app-specific
- Dedicated OpenTelemetry PHP SDK exporter package (protobuf / gRPC) — HTTP JSON ingest is covered by
/api/ingest/trace/otlpand the converter. - PSR-15 “performance” middleware (auto HTTP transactions) — today use manual
Tracing::startTransactionor stay on Laravel. - Queue-based async flush with deduplication across workers.
Scope
Tracing supports manual transactions/spans (Tracing::trace(), startTransaction) and optional performance mode: sampled auto spans for HTTP (middleware), SQL, Artisan, queue, logs, and outbound Guzzle calls, flushed to Lookout’s trace ingest.
Framework instrumentation (above) still records breadcrumbs for error reports; performance collectors add span trees for the distributed trace UI when you flush to /api/ingest/trace.
Crons: Lookout stores check-ins and monitor metadata; it does not yet auto-open issues or email you on missed schedules like Sentry’s hosted monitors—you can build alerting on top (e.g. scheduled jobs reading the API) or extend the app later.