okitsu/zabbix-sender
PHP/Laravel package for sending metrics to Zabbix trapper items using the zabbix_sender protocol. Define host and key values, then push them from your app through a simple API to feed custom monitoring data into Zabbix dashboards, triggers, and alerts. Ideal for app-level telemetry and health checks...
The package is a thin, framework-agnostic implementation of the Zabbix sender protocol plus a parser for zabbix_agentd.conf. It ships no Laravel service provider, facade, or config file, so the first hour of work is wiring it in the way you want it.
composer require okitsu/zabbix-sender
Two classes matter. Read them first:
Zabbix\Sender — opens a TCP socket to the Zabbix server/trapper port (default 10051) and writes the sender protocol frame.Zabbix\AgentConfig — parses an agent config file so you can reuse Server=, Hostname=, ServerActive= instead of duplicating them in .env.Minimal first call:
use Zabbix\Sender;
$sender = new Sender('zabbix.internal', 10051);
$sender->send('web-01', 'app.orders.count', 42);
Before that call works, the Zabbix side must exist: a host whose name exactly matches the first argument, and an item of type Zabbix trapper with the key you used (app.orders.count). Trapper items do not poll — nothing arrives until your app pushes, and a wrong key/host silently drops the value with only a server-side log line.
The first genuinely useful use case for a Laravel app is business/queue metrics that the stock agent cannot see: app.queue.pending, app.orders.created, app.payments.failed, app.http.p95. These are one-liners:
app(Sender::class)->send(config('zabbix.host'), 'app.queue.pending', Queue::size());
Ship that, confirm it lands in Latest data in the Zabbix UI, and only then build the plumbing below.
Bind a singleton with a small config file. Sender is stateless per call, but a singleton avoids re-reading config and keeps the facade ergonomic.
// config/zabbix.php
return [
'server' => env('ZABBIX_SERVER', '127.0.0.1'),
'port' => (int) env('ZABBIX_PORT', 10051),
'host' => env('ZABBIX_HOST', gethostname()),
'timeout' => (int) env('ZABBIX_TIMEOUT', 2),
'enabled' => (bool) env('ZABBIX_ENABLED', true),
];
// app/Providers/ZabbixServiceProvider.php
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../../config/zabbix.php', 'zabbix');
$this->app->singleton(Sender::class, fn ($app) => new Sender(
$app['config']->get('zabbix.server'),
$app['config']->get('zabbix.port'),
));
}
Wrap it in your own class — do not inject Zabbix\Sender directly into controllers. This is the single most important pattern given the package's maturity (36 stars, zero dependents). A ZabbixMetrics service with gauge(string $key, int|float|string $value) lets you swap implementations, add the enabled kill switch, and centralise the try/catch.
Batch aggressively. Every send() is a synchronous TCP round-trip. For a dashboard-load or a batch job, use the multi-item form so all values travel in one frame:
$this->sender->sendMulti($host, [
'app.orders.created' => 12,
'app.orders.failed' => 1,
'app.orders.p95' => 0.42,
]);
Push non-critical metrics off the request path. A queue job is the right home for anything measured in a request but not needed inline:
class SendZabbixMetric implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(public string $key, public mixed $value) {}
public function handle(Sender $sender): void
{
$sender->send(config('zabbix.host'), $this->key, $this->value);
}
}
Bound the job with $tries = 1 and $timeout — a metrics job that retries five times against an unreachable Zabbix server is worse than a lost datapoint.
Use middleware for latency percentiles only if you are sampling. Measuring every request and sending every request will double your outbound connections under load. Aggregate in Redis (INCRBY/HINCRBY) and flush from the scheduler.
Drive everything from the scheduler for periodic metrics. Kernel::schedule() + ->everyFiveMinutes() calling a command that reads queue depth, cache hit ratio, stale-record counts, and sends them in one sendMulti is the highest-value, lowest-risk integration.
Use AgentConfig for host identity in containerised/VM environments where the agent is already installed and configured. Parsing zabbix_agentd.conf for Hostname= avoids the classic "I set ZABBIX_HOST wrong and my values went to /dev/null on the server" failure. Fall back to gethostname() when the file is absent.
Wire it into the exception handler for a app.exceptions.total counter — but only the counter, never the exception payload; trapper text values are length-limited and you do not want stack traces on the wire.
render(). Any network hiccup throws. Always wrap calls in try/catch (\Throwable) inside your wrapper and Log::warning — never let a metrics push 500 a user request.web-01 and web-01.domain.local are different hosts to Zabbix. This is the #1 cause of "the data isn't showing up." Cross-check against Configuration → Hosts before debugging code.Numeric (unsigned), Numeric (float), Text, Log) must match what you send, or the server rejects the value.AgentConfig and Include= directives. Real-world zabbix_agentd.conf files often split settings into included files. If Hostname comes back empty, check whether it lives in an include rather than the main file — verify by reading the file yourself before assuming the parser is broken.zabbix_sender -z <server> -s <host> -k <key> -o <value> -vv from the same machine. If that works, your code is wrong; if it doesn't, your Zabbix config is wrong. Don't debug both at once.composer.json, and keep ZabbixMetrics as an interface so replacing the transport (there are several alternative sender implementations) is a one-file change.ZABBIX_ENABLED=false env var that short-circuits your wrapper is what stops a Zabbix outage from becoming an application outage during an incident.Sender or (better) implement it in your wrapper rather than forking the package.How can I help you explore Laravel packages today?