christian98/datadog-laravel
christian98/datadog-laravel integrates Laravel applications with Datadog, forwarding logs, metrics, and traces to your Datadog account. It ships a service provider, configurable handlers, and helpers for quick, low-effort observability setup.
Install and publish
composer require christian98/datadog-laravel
php artisan vendor:publish --tag=datadog-config # verify the tag name in the provider
The service provider is auto-discovered on Laravel 5.5+. On older versions register Christian98\Datadog\DatadogServiceProvider in config/app.php and alias the facade.
Point it at an agent. This package wraps datadog/php-datadogstatsd, so it speaks UDP/8125 to a local Datadog Agent — it is not an APM tracer. Run the agent (or the datadog/datadog container) and confirm:
docker run -d --name dd-agent -e DD_API_KEY=$DD_API_KEY \
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true -p 8125:8125/udp datadog/agent:latest
First use case — count something you already care about:
use Datadog; // facade, verify the alias the provider registers
Datadog::increment('user.signup', 1, ['plan' => 'pro']);
Datadog::timing('checkout.render', $ms, ['step' => 'payment']);
Where to look first (in this order): config/datadog.php for host/port/namespace/global tags/enabled flag → the service provider's container binding (this tells you whether you get a singleton DogStatsd or a lazily-built client) → the facade class for the exact method surface → the pinned datadog/php-datadogstatsd version in composer.lock, because the SDK's method names and signatures are what you're really programming against.
Wrap it, don't call it everywhere. Because this package is a thin, low-traffic wrapper, the highest-value thing you can do is put your own interface in front of it:
interface Metrics
{
public function increment(string $metric, int $value = 1, array $tags = []): void;
public function timing(string $metric, float $ms, array $tags = []): void;
public function gauge(string $metric, float $value, array $tags = []): void;
}
final class DatadogMetrics implements Metrics { /* delegates to the facade */ }
final class NullMetrics implements Metrics { /* no-ops */ }
Bind Metrics in a provider and choose the implementation off config('datadog.enabled') and app()->environment(). This gives you a no-op in testing/local for free, and lets you swap the transport if the package stalls.
Tag discipline is the whole game. Tags are the only dimension you get; every unique tag combination is a billable time series.
// good — bounded cardinality
Datadog::increment('order.placed', 1, ['plan' => $plan, 'currency' => $currency]);
// bad — one time series per user forever
Datadog::increment('order.placed', 1, ['user_id' => $user->id, 'order_id' => $order->id]);
Anything unbounded (user IDs, request IDs, emails, URLs with IDs, exception messages) belongs in logs or traces, never tags.
Middleware for request timing, registered globally or on a group:
public function handle($request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
Datadog::timing('http.request', (microtime(true) - $start) * 1000, [
'route' => optional($request->route())->getName() ?? 'unmatched',
'method' => $request->method(),
'status' => $response->getStatusCode(),
]);
return $response;
}
Queue jobs — JobProcessing/JobProcessed listeners keyed on $event->job->resolveName(), emitting timing plus separate increment for success/failure. Scheduler — wrap each task in timing and emit a service_check so a silent non-run is visible.
Use distribution, not histogram, when you want percentiles. Histograms on the agent give you avg/max/median and a rough approximation; distributions are aggregated server-side for real p50/p95/p99.
Sample high-volume events rather than emitting per row:
Datadog::increment('cache.hit', 1, ['pool' => 'redis'], 0.1); // verify the sample-rate arg position for your SDK version
Datadog::increment('debug.ping') and check the Metrics Explorer immediately after deploying.datadog/dd-trace configured separately; the two coexist fine but solve different problems.config:cache will bite you. Any env() call outside a config/ file returns null once config is cached. Keep DATADOG_ENABLED, DATADOG_HOST, DD_AGENT_HOST reads inside config/datadog.php only.127.0.0.1 inside your app container is not the host. Set DATADOG_HOST=host.docker.internal (or the agent's compose service name) and remember DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true — it defaults to false and silently drops external packets.'myapp.orders'), you get myapp.myapp.orders. Pick one convention and grep for it.scoped() binding or periodically re-resolving the client. Also call the SDK's flush method (if your pinned version has one) at the end of long CLI commands, since batched packets may not be sent.timing is milliseconds. Passing microtime(true) (seconds) yields a dashboard reading ~1000× low and looks like a "traffic drop."NullMetrics in the test environment and assert on your interface with a spy, rather than trying to inspect UDP. If you must integration-test, point the client at a local UDP listener socket.composer require christian98/datadog-laravel:^x.y, never dev-main. The abstraction layer from Implementation Patterns is your exit ramp — replacing it with a 40-line service binding directly to DataDog\DogStatsd is trivial if the package goes unmaintained.How can I help you explore Laravel packages today?