graze/dog-statsd
Graze DogStatsD is a PHP client for Datadog's DogStatsD protocol, making it easy for Laravel and PHP apps to send counters, gauges, timers, sets, and events with tags. It provides a simple API for instrumenting application metrics via Datadog's statsd agent.
Install and confirm the package name
composer require graze/dog-statsd
The repository is not linked in the metadata, so after install go straight to vendor/graze/dog-statsd/README.md and vendor/graze/dog-statsd/src/Client.php. The README is the only reliable source for the exact constructor signature and supported metric methods; the package is old enough that blog posts and framework examples may not match.
Put a DogStatsD endpoint in front of it
The client is a UDP sender, not a Datadog API SDK. In local development run the Datadog agent (or a StatsD-compatible listener) and expose UDP 8125. In containers make sure the agent’s dogstatsd port is reachable and DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true if the agent runs in a separate container.
Smoke-test in tinker before wiring anything into Laravel
$client = new \Graze\DogStatsD\Client('127.0.0.1', 8125);
$client->increment('app.boot');
$client->timing('app.boot_time', 12.4, ['env' => 'local']);
In another terminal watch packets with sudo tcpdump -i lo -n udp port 8125 or nc -ul 8125. If you see datagrams, the rest is plumbing. If not, you are debugging networking, not Laravel.
First production use case: request timing middleware The highest-value/lowest-risk first metric is HTTP request duration and response count. Send it from middleware, guard it with a config flag, and wrap it so metrics can never fail a request. Once that appears in Datadog, expand into jobs, queue depth, cache hits, and business events.
Create explicit configuration rather than hard-coding The package has no Laravel service provider or config file, so add your own:
// config/statsd.php
return [
'enabled' => env('STATSD_ENABLED', true),
'host' => env('STATSD_HOST', '127.0.0.1'),
'port' => env('STATSD_PORT', 8125),
'prefix' => env('STATSD_PREFIX', 'myapp'),
'tags' => [
'env' => env('APP_ENV', 'local'),
'service' => env('APP_NAME', 'laravel'),
'version' => env('APP_VERSION', 'dev'),
],
];
Bind the client as a singleton, but only behind an interface
Register it in AppServiceProvider (or a small dedicated provider), and inject the package interface if one exists, otherwise a thin local wrapper. This keeps the old package from leaking throughout the codebase.
$this->app->singleton(\Graze\DogStatsD\Client::class, function () {
return new \Graze\DogStatsD\Client(
config('statsd.host'),
config('statsd.port')
);
});
Wrap it in an application-specific metrics class
The most common long-term mistake is scattering raw $client->increment('...') calls everywhere. Centralize metric names, default tags, prefixing, and enable/disable logic:
final class Metrics
{
public function __construct(
private \Graze\DogStatsD\Client $client,
private array $defaultTags,
private string $prefix,
private bool $enabled,
) {}
public function increment(string $metric, array $tags = [], float $sampleRate = 1.0): void
{
if (! $this->enabled) {
return;
}
try {
$this->client->increment(
$this->prefix . '.' . $metric,
$sampleRate,
array_merge($this->defaultTags, $tags)
);
} catch (\Throwable $e) {
report($e); // never let observability break the request
}
}
}
Verify the argument order against src/Client.php; older clients often use (metric, sampleRate, tags), not (metric, tags, sampleRate).
Use middleware with terminate() for request metrics
Measure and count in handle(), but emit after the response has been sent so metrics do not add latency:
public function handle($request, Closure $next)
{
$request->attributes->set('_statsd_start', microtime(true));
return $next($request);
}
public function terminate($request, $response): void
{
if (! $request->attributes->has('_statsd_start')) {
return;
}
$ms = (microtime(true) - $request->attributes->get('_statsd_start')) * 1000;
$this->metrics->timing('http.request.duration', $ms, [
'route' => $request->route()?->getName() ?? 'unknown',
'method' => $request->method(),
'status' => (string) $response->getStatusCode(),
]);
$this->metrics->increment('http.request.count', [
'status' => (string) $response->getStatusCode(),
]);
}
Instrument jobs and queue workers
Use Laravel job events rather than editing every job: JobProcessing/JobProcessed/JobFailed. Emit queue.job.duration, queue.job.count, and queue.job.failed with job and queue tags. Avoid tagging with job IDs or user IDs.
Use tags for dimensions, not metric names
Prefer orders.created + ['plan' => 'pro', 'country' => 'GB'] over orders.created.pro.gb. Datadog can slice and aggregate tags, and you avoid an uncontrollable metric-name explosion. Keep the tag set small and bounded; treat route, status, queue, job, and env as safe; treat user_id, order_id, and raw URLs as unsafe.
Adopt sampling deliberately for high-volume metrics
For hot paths (cache hits, per-query timings), pass a sample rate such as 0.1. Remember that sampling changes the count Datadog reports unless the client and agent extrapolate it; validate in a staging environment before relying on absolute counts.
Use events and service checks for deploys and health
The package is a DogStatsD client, so it should support Datadog events and service checks. Use events for deployments/releases and service checks for dependency health (database, Redis, queue lag). Wire these into deployment scripts and health-check commands rather than controllers.
Provide a safe testing implementation
In phpunit.xml set STATSD_ENABLED=false, and bind a no-op or spy implementation in TestCase. If you have a Metrics wrapper, test that it emits the right metric names and tags by injecting a fake client, not by opening UDP sockets.
Use one client per process, not per service
Constructing a UDP client per controller/service call is unnecessary. Resolve the singleton, and in Laravel Octane verify whether the client holds a long-lived socket safely across requests. If it does not, rebuild it inside the request or use the wrapper’s per-call send(), but document the decision.
The package is old and thin, not a framework integration. Last release was April 2021; there is no Laravel auto-discovery, config publishing, facade, or Octane-specific handling. Check composer why-not graze/dog-statsd and composer show graze/dog-statsd for PHP-version constraints before adopting it on PHP 8.2+. The NOASSERTION license metadata also means you should confirm the actual license in composer.json/LICENSE before distributing or using it commercially.
UDP is fire-and-forget. There is no acknowledgement, no retry, and no guarantee of delivery. Metrics must never be used as the source of truth for business logic, billing, or auditing, and every send should be wrapped in try/catch or gated by statsd.enabled.
Argument order and method names vary between versions. Confirm in the source whether the signature is timing($metric, $ms, $tags, $sampleRate) or timing($metric, $ms, $sampleRate, $tags). A silently wrong order produces either malformed packets or tags interpreted as sample rates. Write one integration test against a fake UDP socket or against nc -ul and lock the order down.
Metric and tag names are constrained. Metric names cannot contain : (it separates value and type). Tag keys/values cannot contain , or |, and the # prefix is structural. Sanitize route names, job classes, and user-provided strings before tagging.
Timing units are milliseconds. Passing seconds is the most common instrumentation bug and produces charts that look plausible but are wrong by 1000×. Be explicit in wrapper method names: recordDurationMs(), not recordTime().
Watch UDP datagram size. Keep tag sets small; oversized packets can be fragmented or dropped. Datadog’s dogstatsd default max datagram is around 1432 bytes. Do not attach huge tag values (full URLs, exception messages, SQL) to every metric; use logs or traces for that.
High-cardinality tags are a silent cost/latency problem. Tagging every request with user_id, session, or url will explode Datadog’s custom metric volumes. Use route patterns, status classes, and a small set of allow-listed dimensions.
gauge is not a counter. A gauge reports the latest absolute value; repeated gauge sends overwrite. Use a count/increment for event totals, and gauges for things like queue depth or memory usage. Be careful if you need delta-style gauges; not all clients support +/- gauge syntax.
Sample rates should be intentional. If you pass 0.1 for a hot path, understand whether the client and agent adjust counts. Test with a known number of calls and compare Datadog’s reported values.
Localhost vs. DNS resolution. Prefer an IP address or a stable Docker service name; a client that resolves per call can add DNS latency to hot paths. For IPv6, use bracketed addresses ([::1]).
Debugging is easiest at the socket layer. If metrics don’t appear: (1) tcpdump -i lo -n udp port 8125 on the app host, (2) check the agent’s dogstatsd listener and non-local traffic setting, (3) check whether the app and agent share a network namespace, (4) confirm the metric prefix in Datadog’s Metrics Summary, and (5) look for tags with invalid characters silently dropping points.
Do not emit from a request without a guard. If the StatsD agent is down, a naive client can still block or warn heavily on every request. Configure the wrapper to catch all exceptions, log once/rate-limit warnings, and keep the request path unaffected.
Queue and scheduler contexts need their own wiring. Middleware only covers HTTP. Instrument jobs and scheduled commands separately, and remember that long-running queue workers keep the same singleton client across many jobs, so any per-job tags must be passed explicitly rather than set as mutable client state.
Extension point is the wrapper, not the package. Because the package is unmaintained relative to current Laravel/PHP, isolate it behind your own Metrics abstraction. That way you can swap in dataDog/datadog-php or a newer StatsD client without changing controllers, jobs, or middleware.
How can I help you explore Laravel packages today?