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).
README
PHP library for Lookout distributed tracing: compact traceparent-style propagation, W3C baggage, manual transactions/spans, and optional Laravel integration. Wire formats stay compatible with common PHP tracing clients without naming third-party vendors here.
Install
Requirements: PHP 8.3+ and Composer. You always add the library with Composer; only Laravel gets auto-wiring via a service provider.
Laravel application
Run these from your Laravel project root (the directory that contains artisan and composer.json):
-
Add the package
cd /path/to/your-laravel-app composer require lookout/tracingComposer updates
composer.json/composer.lockand downloadslookout/tracingintovendor/. -
Configure Lookout (pick one path)
- Interactive (recommended):
php artisan lookout:install— choose Create a new project (Lookout web URL + your API token from Profile → API tokens) or Use an existing DSN. Create flow calls Lookout’s API (GET /api/v1/me,POST /api/v1/projects), then writesLOOKOUT_DSNusing the new project’s ingest key. It also appendsLOOKOUT_LARAVEL=trueto.env. - Manual: add to
.envyourself (see Quick install under Laravel below), or setLOOKOUT_API_KEY+LOOKOUT_URLif your team shares one Lookout host.
- Interactive (recommended):
-
Clear config cache (if you use it in this environment)
php artisan config:clear
-
Optional — publish config when you need every env knob (sampling, middleware, log/metrics toggles):
php artisan vendor:publish --tag=lookout-tracing-config
-
Tracing middleware — for distributed traces, register
lookoutTracing.continueTrace(and optionallylookoutTracing.performance) on yourweb/apistacks, or useLOOKOUT_PERFORMANCE_AUTO_MIDDLEWARE=truefor the performance middleware only (see Performance monitoring).
Laravel auto-discovers Lookout\Tracing\Laravel\LookoutTracingServiceProvider; you do not add it to config/app.php manually.
Other PHP projects (Symfony, Slim, custom apps, libraries)
There is no Laravel service provider. Install the same Composer package and configure the tracer in your bootstrap (or a DI container):
cd /path/to/your-php-project
composer require lookout/tracing
Then wire Lookout ingest explicitly, for example:
use Lookout\Tracing\Tracer; Tracer::instance()->configure([ 'api_key' => getenv('LOOKOUT_API_KEY') ?: null, 'base_uri' => rtrim((string) getenv('LOOKOUT_URL'), '/') ?: null, 'environment' => getenv('APP_ENV') ?: null, ]);
Use Tracer, Tracing, lookout_logger(), lookout_metrics(), PSR-15 middleware (e.g. ContinueTracePsr15Middleware), and GuzzleTraceMiddleware as needed; see Propagation, Custom instrumentation, Lookout ingest, and Guzzle 7 below. For Slim / Mezzio, see the slim/slim suggestion in composer.json.
(This monorepo vendors the package from packages/lookout-tracing via a Composer path repository when developing Lookout itself.)
Propagation
- Incoming: parse the compact trace header (see
Lookout\Tracing\TraceWireHeaders::HTTP_TRACEPARENT) andbaggage(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; use Lookout\Tracing\TraceWireHeaders; Tracer::instance()->continueTrace( $request->getHeaderLine(TraceWireHeaders::HTTP_TRACEPARENT), $request->getHeaderLine(TraceWireHeaders::HTTP_BAGGAGE), ); $headers = Tracer::instance()->outgoingTraceHeaders(); // keys: TraceWireHeaders::HTTP_TRACEPARENT, TraceWireHeaders::HTTP_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
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; enabled from the dashboard (Monitoring → Signals), with LOOKOUT_LOGS_ENABLED as a local override (env > site). 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
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. Enabled from the dashboard (Monitoring → Signals), with LOOKOUT_METRICS_ENABLED as a local override (env > site). 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).
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 | Sampled SQL breadcrumbs (default on); DB spans with fingerprint, bindings, slow flag + root rollup (db.total_duration_ms, db.slow_query_count) when performance DB collector on |
| Database transactions | Breadcrumbs when instrumentation.database_transactions or comprehensive_collection |
| Cache events | Cache breadcrumbs (default on) + cache spans (default on when performance enabled) |
| 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 → Signals turns each signal type on/off per project while leaving error ingest always on.
Signal control: dashboard + env overrides
Which signals are captured/sent is controlled from the Lookout dashboard (Project → Monitoring → Signals) and fetched by the SDK at boot via GET /api/config (cached for LOOKOUT_REMOTE_CONFIG_TTL, default 300s; disable the whole mechanism with LOOKOUT_REMOTE_CONFIG=false). Sample rates sync the same way. This replaced the per-signal LOOKOUT_*_MONITORING_ENABLED / *_ENABLED env vars as the primary switch and the old LOOKOUT_PERFORMANCE_SYNC_FROM_API (+ LOOKOUT_SYNC_API_TOKEN, LOOKOUT_SYNC_PROJECT_ID), which are gone.
Precedence is env > site: an explicitly-set LOOKOUT_*_ENABLED / *_SAMPLE_RATE still wins over the dashboard. When env force-enables a signal the dashboard has off, the SDK sends X-Lookout-Env-Forced so the server accepts it, and reports its env overrides on the config fetch so the dashboard shows them as "Set by env."
User feedback (crash page)
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
Monitor check-ins: 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)
Automatic (Laravel + Excimer) — zero code
If the Excimer PECL extension is installed, turn on auto-profiling and the package captures a
sampled fraction of web requests, console commands, and queue jobs and uploads them for you — no
ProfileClient::sendProfile() calls. Without Excimer this is a silent no-op (use the manual paths below),
and it never throws into your app.
LOOKOUT_PROFILING_ENABLED=true LOOKOUT_PROFILING_SAMPLE_RATE=0.05 # 5% of transactions (keep low — each profile counts toward your event quota) LOOKOUT_PROFILING_PERIOD_US=10000 # sample period in microseconds (10ms) LOOKOUT_PROFILING_EVENT_TYPE=wall # 'wall' (default) or 'cpu' LOOKOUT_PROFILING_MIN_DURATION_MS=0 # only upload transactions at least this slow
Auto-profiling rides on performance instrumentation, so keep performance.enabled = true. trace_id,
transaction, environment, and release are attached automatically so profiles cross-link from traces
and errors. For non-Laravel apps or when Excimer is unavailable, capture manually:
Manual capture
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.
Quick install
composer require lookout/tracing php artisan lookout:install
lookout:install can either create a project on your Lookout instance (API token from Profile → API tokens + base URL) or use an existing DSN. Either way it appends to .env:
LOOKOUT_DSN="https://YOUR_PROJECT_API_KEY@your-lookout-host.example.com" LOOKOUT_LARAVEL=true
LOOKOUT_DSN— single line:https://+ project ingest API key as the URL user +@+ Lookout host (optional port). Percent-encode the key if it contains@or other reserved characters. The create-project flow obtains this key from the API afterPOST /api/v1/projects.LOOKOUT_LARAVEL=true— enables uncaught exception reporting (LOOKOUT_REPORT_EXCEPTIONS) and trace auto-flush on HTTP terminate (LOOKOUT_TRACING_AUTO_FLUSH) unless you override those env vars explicitly.
Non-interactive:
- Existing project:
php artisan lookout:install --dsn="https://PROJECT_KEY@host.example.com". - New project:
php artisan lookout:install --url="https://host.example.com" --token="your_api_token"(and--organization=ULIDif your account has more than one organization). Optional--project-name="My App". On the same Laravel host (e.g. this Lookout app), you can omit--urlwhenAPP_URLis set:--token="…"alone uses that origin.
Pass --no-quick to skip LOOKOUT_LARAVEL=true.
API key only (team shares one Lookout URL): set a default host once — LOOKOUT_URL, LOOKOUT_BASE_URI, or config/services.php → lookout.url — then each environment only needs LOOKOUT_API_KEY.
- Middleware alias:
lookoutTracing.continueTrace— callcontinueTrace()from incoming headers. - Publish config:
php artisan vendor:publish --tag=lookout-tracing-config - Env resolution order for base URI:
LOOKOUT_DSNhost →LOOKOUT_BASE_URI→LOOKOUT_URL→config('services.lookout.url')→APP_URL. 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.
With LOOKOUT_LARAVEL=true or LOOKOUT_REPORT_EXCEPTIONS=true (and a resolved API key + base URI), the provider registers 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)
Enabled from the dashboard (Monitoring → Signals), with LOOKOUT_PERFORMANCE_ENABLED as a local override (env > site); needs a resolved API key and base URI from LOOKOUT_DSN, LOOKOUT_API_KEY + LOOKOUT_URL, etc. 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. If the project has trace ingest off in Monitoring → Signals the SDK stops sending (or sends X-Lookout-Env-Forced when env forces it on); 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 from an incoming traceparent withsampled=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 lowering head sample rates in 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 and compact traceparent propagation.
| Server behavior | SDK support |
|---|---|
performance_ingest_enabled false |
Trace ingest returns 403. The SDK reads this from remote config (see below) and stops sending traces to match the dashboard — no 403 spam. Override locally with LOOKOUT_PERFORMANCE_ENABLED (env > site); when env forces it on, the SDK sends X-Lookout-Env-Forced so the server accepts it. Auto-flush and queue/cli flush log lookout.tracing.trace_forbidden when performance.log_forbidden_trace_ingest is true (default). |
GET /api/config |
RemoteConfig::fetch() — per-project enabled flags + sample rates, cached and applied at boot (replaces the old performance.sync_from_api / GET /api/v1/projects/{id} path). |
| 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-15 traceparent /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 some hosted cron products—you can build alerting on top (e.g. scheduled jobs reading the API) or extend the app later.