pkerrigan/xray
Xray is a Laravel package for adding feature flags to your app. Define flags, toggle them at runtime, and gate features in PHP or Blade without redeploying—useful for gradual rollouts, beta access, and safely shipping incomplete work.
Install and prerequisites
composer require pkerrigan/xray
This is a thin, dependency-light instrumentation library — not an agent and not a Laravel package. Before writing code you need somewhere for segments to go:
127.0.0.1:2000/udp).xray:PutTraceSegments and xray:PutTelemetryRecords.composer.json for the supported PHP range before adopting.Where to look first
README.md — the usage example is essentially the whole API surface.src/Trace.php — the root segment and lifecycle (begin(), end(), submit()).src/AbstractSegment.php / src/Segment.php / src/Subsegment.php — naming, timing, annotations, metadata, error flags, and addSubsegment().src/Submission/DaemonSegmentSubmitter.php — the default emitter; the place to look when tracing silently does nothing.src/Submission/SegmentSubmitter.php — the interface to implement for a custom transport or test double.First use case: one trace per request, one subsegment around a DB call
use Pkerrigan\Xray\Trace;
use Pkerrigan\Xray\Subsegment;
$trace = new Trace();
$trace->setName(config('app.name'));
$trace->begin();
$sub = new Subsegment();
$sub->setName('users.find');
$sub->begin();
try {
$user = User::findOrFail($id);
} catch (\Throwable $e) {
$sub->setError(true); // verify method name in src/AbstractSegment.php
throw $e;
} finally {
$sub->end();
}
$trace->addSubsegment($sub);
$trace->end();
$trace->submit(); // uses DaemonSegmentSubmitter by default
Verify in the X-Ray console that the trace appears. If it does not, the problem is almost always the daemon address, IAM, or an unclosed segment — not the PHP code.
1. Laravel middleware lifecycle
Register a global or route middleware that starts the trace in handle() and submits it in terminate(). Bind the trace in the container so controllers, jobs, and services can add subsegments without passing it around.
public function handle($request, Closure $next)
{
$trace = new \Pkerrigan\Xray\Trace();
$trace->setName(config('app.name'));
$trace->setUrl($request->fullUrl());
$trace->setMethod($request->method());
$trace->begin();
app()->instance('xray.trace', $trace);
return $next($request);
}
public function terminate($request, $response)
{
$trace = app('xray.trace');
$trace->addAnnotation('http.status', $response->getStatusCode());
$trace->end();
$trace->submit();
}
Use terminate() rather than the end of handle() so the trace covers the full response lifecycle. In Laravel, register it in bootstrap/app.php (Laravel 11+) or Kernel.php.
2. Wrap work in a helper
Manual instrumentation gets noisy. A helper keeps try/finally correct and encourages consistent naming:
function xray_segment(string $name, callable $fn, array $annotations = [])
{
$trace = app('xray.trace');
$sub = new \Pkerrigan\Xray\Subsegment();
$sub->setName($name);
foreach ($annotations as $k => $v) {
$sub->addAnnotation($k, $v);
}
$sub->begin();
try {
return $fn();
} catch (\Throwable $e) {
$sub->setError(true);
throw $e;
} finally {
$sub->end();
$trace->addSubsegment($sub);
}
}
Then: xray_segment('stripe.charge', fn () => $stripe->charges->create(...)).
3. Inbound header propagation
X-Ray clients send X-Amzn-Trace-Id: Root=1-...;Parent=...;Sampled=1. Parse it at the edge and attach it to your trace so upstream and downstream segments join the same trace. Read the request header, split on ;, validate the Root format, and set trace ID, parent ID, and sampled flag on your Trace. If the package exposes a header helper, prefer it over hand-parsing.
4. Outbound HTTP propagation
Add a Guzzle middleware (or Laravel HTTP client macro) that serializes the current trace header onto outbound requests:
$stack->push(Middleware::mapRequest(function ($request) {
$trace = app('xray.trace');
return $request->withHeader('X-Amzn-Trace-Id', $trace->getHeader());
}));
Also create a subsegment for the call, named after the downstream service, with namespace = remote if the API supports it. This is where most of the day-to-day value comes from: seeing the service map and latency between your app and external dependencies.
5. Queue/job propagation
Don't share a live segment object across processes. Serialize the trace header into the job payload, then in the worker create a new trace with the parent ID set from that header, start it, run the job, and submit. The web request should not stay open waiting for the job.
6. Custom submitter for testing or alternate transport
Implement SegmentSubmitter and either forward to the daemon, buffer to a file, or capture in memory for assertions. In tests, a recording submitter lets you assert segment names, annotations, and error flags without any AWS infrastructure. This is also the cleanest way to debug what JSON the library actually produces.
7. Sampling decisions
Decide early whether the request is sampled. If not sampled, return a no-op trace path and avoid building segments — the cost of instrumentation should be near-zero for unsampled traffic. Respect an upstream Sampled=0; don't override it.
try/finally. A subsegment that is begin()-ed but never end()-ed will either be dropped or produce an invalid document.docker logs, or temporarily swap in a TCP/file submitter to confirm payloads.127.0.0.1:2000 is correct only for sidecar/localhost. In Docker Compose or Kubernetes you may need the sidecar hostname or the node IP.Root, Parent, and Sampled are semicolon-separated. Root trace IDs have a specific 1-8hex-24hex shape. Malformed headers silently break trace continuity; validate before applying.error/fault on the segment. Set the flags explicitly in your catch blocks, otherwise failing requests look healthy in X-Ray.submit() more than once (e.g., in both middleware and a shutdown function) duplicates traces or produces partial documents. Pick a single lifecycle owner.Trace per request/job and reset any static state.composer.json, and don't expect rapid fixes; the codebase is small enough that forking a submitter or segment class is realistic.Segment/Subsegment to set default namespace or naming conventions, or implement SegmentSubmitter to change transport. Those two hooks cover most nonstandard environments.end(), wrong parent IDs, or a malformed trace header.How can I help you explore Laravel packages today?