google/cloud-trace
Official Google Cloud Trace client for PHP. Capture and report distributed traces from your applications to Google Cloud, helping you analyze request latency, find bottlenecks, and debug performance across services. Supports OpenCensus and easy integration.
This is a thin, GAPIC-generated client for the Cloud Trace API — not an auto-instrumentation agent. There is no service provider, no middleware, no Trace::startSpan() facade. You are building the instrumentation layer yourself. Budget an hour to scaffold a wrapper, not five minutes to composer require.
Install and authenticate
composer require google/cloud-trace
Auth is Application Default Credentials only. Locally:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
export GOOGLE_CLOUD_PROJECT=my-project-id
On GKE/Cloud Run/App Engine the metadata server is used automatically — do not ship a key file. The service account needs roles/cloudtrace.agent.
Where to look first
Google\Cloud\Trace\TraceClient + Google\Cloud\Trace\Trace + Google\Cloud\Trace\Span — the hand-written convenience layer. Start here.Google\Cloud\Trace\V2\TraceServiceClient (or ...\V2\Client\TraceServiceClient depending on version) — the GAPIC client. This is where batchWriteSpans() lives, and it is what you will end up using in production.First use case: trace one Artisan command
use Google\Cloud\Trace\TraceClient;
$client = new TraceClient(['projectId' => config('services.gcp.project')]);
$trace = $client->trace(); // generates a 32-hex trace ID
$span = $trace->span(['name' => 'nightly:reconcile-orders']);
$span->start();
// ... work ...
$span->stop();
$client->insert($span); // single-span write, fine for cron jobs
Then open Trace → Trace Explorer in the GCP console, filter by span name, and confirm it landed before building anything else. If the span doesn't appear, the problem is IAM or project ID 95% of the time, not your code.
Bind one client as a singleton. Don't construct TraceClient per call — it holds a gRPC/HTTP transport.
// AppServiceProvider::register()
$this->app->singleton(TraceClient::class, fn () => new TraceClient([
'projectId' => config('services.gcp.project'),
]));
Wrap it in your own small API. The raw client's shape leaks Google-specific concepts (AttributeValue, TimeEvents) into every call site. A 60-line Tracer class with startSpan(string $name, array $attrs = []): SpanHandle pays for itself immediately, and lets you swap to OpenTelemetry later without touching call sites.
Request middleware — the highest-value integration. Register a global middleware that opens a root span named after the route and closes it in terminate():
public function handle($request, Closure $next)
{
$this->tracer->startRoot('http ' . $request->method() . ' ' . $request->route()?->uri());
return $next($request);
}
public function terminate($request, $response)
{
$this->tracer->finishRoot(['http.status_code' => $response->getStatusCode()]);
$this->tracer->flush(); // runs after the response is sent
}
Laravel calls terminate() after the response is flushed to the client, which is exactly where you want the network write to happen. Never write spans inline in handle().
Batch aggressively. batchWriteSpans() takes up to 1000 spans in one call. Buffer spans in the tracer, flush on terminate() (web) or at the end of handle() (queue worker, where you should flush every N jobs, not every job).
Propagate context across jobs and HTTP. Use the W3C traceparent header: 00-{32 hex trace id}-{16 hex span id}-01. Inject it into queued job payloads and outgoing Http::withHeaders(). You will hand-roll the parent-span relationship — the package has no notion of "current span."
Instrument selectively. Realistic targets: queue job dispatch/processing, outbound HTTP (Http::globalMiddleware), Eloquent slow queries, and any external service call. Do not span every controller — you'll blow through the free tier and make the waterfall unreadable.
Sample client-side. There is no server-side sampling knob on this client. Roll a decision at the root and propagate it (a sampled boolean in your tracer) so child spans are consistent:
$sampled = $request->header('X-Force-Trace') || random_int(1, 100) <= 5;
Testing. Bind a fake Tracer in the container. Never let CI hit the Trace API — it's slow, costs money, and fails without credentials. Assert on spans recorded by the fake instead.
The package is effectively in maintenance mode. Cloud Trace itself is healthy, but Google's supported path for PHP is OpenTelemetry (open-telemetry/sdk + open-telemetry/exporter-otlp pointed at the Cloud Trace OTLP endpoint) or the auto-instrumentation extension. With 23 stars and near-zero dependent count, treat google/cloud-trace as "use it if you specifically need direct API calls"; otherwise prefer OTel. Design your Tracer wrapper so this swap is a one-file change.
ID formats are strict and silently rejected. Trace ID = exactly 32 lowercase hex chars (16 bytes); span ID = exactly 16 (8 bytes). All-zeros span ID is invalid. Generate with bin2hex(random_bytes(16)) and bin2hex(random_bytes(8)) — uniqid() and UUID-with-dashes will fail with an opaque InvalidArgument.
Spans must be written promptly. The API rejects spans whose end time is far in the past. Don't buffer for minutes in a queue worker hoping to batch — flush within seconds of the span ending. Same for clock skew: truncate timestamps to microseconds and prefer the API's Timestamp message over string dates.
Attributes need explicit types. Every value is an AttributeValue with exactly one of string_value / int_value / bool_value set. PHP nulls, floats, arrays, and objects are not accepted. Floats in particular must be stringified — a silent source of "why is my span missing attributes."
Limits: ~32 attributes per span, span name ≤128 bytes, attribute key ≤128 bytes, value ≤256 bytes. High-cardinality attributes (user IDs, full URLs with query strings, SQL text) are the classic mistake — truncate or hash them.
insert() vs batchWriteSpans(). The convenience layer's single-span insert is a separate RPC per span. At 15 spans per request that's 15 round trips' worth of latency if you ever get the flushing wrong. Default to batching.
FPM/Octane: under Octane the container and singleton client persist across requests — make sure your tracer buffer is reset (scoped binding, $this->app->scoped(Tracer::class)) or you'll leak spans between requests and produce nonsensical waterfalls.
Debugging checklist, in order: (1) span returned from batchWriteSpans without exception? (2) correct project ID from the console's project picker? (3) SA has cloudtrace.agent? (4) timing — did you flush before the process exited? A CLI script that ends immediately after stop() without a flush writes nothing. (5) the Trace Explorer's time range defaults are shorter than you think.
Extension point worth knowing: if you truly can't use OTel yet, the cleanest intermediate is a PSR-15-ish middleware plus an OTLP exporter writing to https://cloudtrace.googleapis.com/v1/traces — the OTLP endpoint accepts standard OTLP/HTTP JSON, so you get vendor-neutral span creation with Cloud Trace as the backend, and drop this package entirely.
How can I help you explore Laravel packages today?