exolnet/laravel-graylog
Exolnet's Laravel Graylog package integrates Graylog with Laravel's logging system via a GELF handler, sending application logs to Graylog. Configure host, port, and transport for centralized log collection and search.
Install and wire it up (5 minutes):
composer require exolnet/laravel-graylog:^1.15
php artisan vendor:publish --tag=graylog # writes config/graylog.php
The service provider is auto-discovered. Add a channel in config/logging.php:
'graylog' => [
'driver' => 'graylog',
'host' => env('GRAYLOG_HOST', 'localhost'),
'port' => env('GRAYLOG_PORT', 12201),
'transport' => env('GRAYLOG_TRANSPORT', 'udp'),
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('GRAYLOG_FACILITY', env('APP_NAME')),
],
Confirm the exact key names against the published config/graylog.php — they are the source of truth, not this snippet.
Where to look first:
config/graylog.php — transport, host/port, facility, default level.config/logging.php — the channel entry (this is what you actually tune day-to-day).vendor/exolnet/laravel-graylog/src — the service provider and the handler/transport factory, which is where you go when a message silently disappears.First use case: dual-write production logs to disk and Graylog, so you keep local fallback when the collector is unreachable:
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'graylog'],
],
Then .env: LOG_CHANNEL=stack, LOG_LEVEL=info. Verify with:
Log::channel('graylog')->info('assessment smoke test', ['order_id' => 42]);
Search Graylog for order_id:42. If nothing appears and the local file has the line, the problem is transport/network, not Laravel.
Version floor: v1.15.0 adds Laravel 12 and PHP 8.4 support and drops Laravel 10. If you're on Laravel 10, stay on ^1.14 — do not float the constraint.
Stack, don't replace. Emit to Graylog alongside single/daily in every environment except local, where the collector usually isn't running. Gate it on config rather than code:
'channels' => env('GRAYLOG_ENABLED', false)
? ['single', 'graylog']
: ['single'],
Request-scoped context via middleware. Graylog's value is structured search, so push correlation data once instead of sprinkling it through every call:
Log::withContext([
'request_id' => (string) Str::uuid(),
'user_id' => optional(auth()->user())->id,
'route' => request()->route()?->getName(),
]);
Put this early in the web/api middleware group. It applies to every channel in the stack.
Separate channels for separate streams. Use multiple Graylog channels with different facility values or levels — e.g. a security channel at warning and an audit channel at info — then route Graylog streams off the facility rather than writing complex extractors.
Log::channel('security')->warning('Failed login', ['email' => $email, 'ip' => $ip]);
Exceptions. Keep the default report() behaviour into the stack; you get the exception class, message, file/line in Graylog for free. For noisy third-party exceptions use $dontReport / $this->reportable() rather than filtering downstream.
Testing. Never let tests hit a real collector. In phpunit.xml set GRAYLOG_ENABLED=false, and assert on the channel rather than the wire:
Log::spy();
$this->post('/orders');
Log::shouldHaveReceived('warning')->withArgs(fn ($m) => str_contains($m, 'failed'));
If you need to assert the actual channel name resolves, use Log::channel('graylog') inside a test that swaps the underlying handler for a TestHandler — a misconfigured host will otherwise surface as a slow test, not a failure.
Short-lived processes. CLI commands, queue workers and Octane workers all go through the same channel; there's no batching layer, so a job logging 10k lines issues 10k datagrams. Throttle at the call site or drop to daily files for high-volume jobs and ship those out-of-band.
udp gives you no delivery guarantee and no error when a datagram is dropped. Also watch size: a fat exception trace plus large context can exceed the datagram/MTU budget and vanish or fragment. Use tcp (or tls) for production and anything with stack traces; reserve udp for low-value high-volume chatter.^1.14 if you're still on L10, and check your composer constraint isn't ^1.0 on a shared package.env() dies under config:cache. Only reference env vars inside config files (as above). A channel defined inline in a service provider with env() will break in production after php artisan config:cache.config:cache on deploy after changing hosts, ports, or transport. Stale cached config pointing at the old collector is the #1 "why are logs missing" cause.level never leaves the app — you cannot recover it in Graylog. Set LOG_LEVEL=debug temporarily via env rather than editing config if you need to widen the net during an incident.['payload' => json_encode($data)].Log::channel('graylog') resolve without throwing? (2) is the message in storage/logs/laravel.log via the stack? (3) can the host reach GRAYLOG_HOST:GRAYLOG_PORT — nc -zvu host 12201 for UDP, nc -zv for TCP? (4) check the Graylog input is bound to 0.0.0.0, not 127.0.0.1. Only then look at the package source.logging.php, so you can register your own driver key pointing at a subclassed handler, or bind a custom GELF publisher/transport in a service provider if you need a proxy, retry logic, or a non-standard transport. Override the formatter if you need extra default fields on every message rather than relying on Log::withContext().Log facade with an explicit channel name; there is nothing package-specific to learn beyond the channel config.How can I help you explore Laravel packages today?