trademachines/riemann-client-php
A PHP client for Riemann, the event stream processor for monitoring distributed systems. Send events, metrics, and state updates from your Laravel or PHP applications to a Riemann server with a simple, expressive API.
Install and confirm the shape of the API first. This is a thin, low-traffic package (0 dependents), so treat the repository itself as the documentation of record:
composer require trademachines/riemann-client-php
Before writing any integration code, open these in order:
README.md — install snippet and the canonical "send one event" example.src/ — find the client entry point (typically a Client class with send() / sendEvents()).src/Proto/ or similar) — this tells you exactly which Event fields exist (host, service, state, metric, description, tags, ttl, attributes, time) and their types.composer.json — PHP version constraint and whether ext-protobuf is required or whether the messages are pure-PHP generated classes. This determines your deploy image.Riemann speaks Protocol Buffers over a raw TCP socket (default port 5555), so your first smoke test needs a reachable Riemann instance — locally that is usually docker run -p 5555:5555 -p 4567:4567 riemann/riemann.
First use case: emit a single event and see it land. Start with one event rather than a metrics pipeline, because the fastest way to validate the whole chain (socket → protobuf → Riemann index) is to look at the event in Riemann's dashboard or query it from the CLI:
$client = new \Riemann\Client('127.0.0.1', 5555);
$event = new \Riemann\Proto\Event();
$event->setHost(gethostname());
$event->setService('myapp.heartbeat');
$event->setState('ok');
$event->setMetric(1.0);
$event->setTtl(60.0);
$client->send($event);
Confirm it arrived before building anything on top:
riemann-client query 'service = "myapp.heartbeat"'
# or open http://localhost:4567 in riemann-dash
Note: construct-then-set (or a fluent builder, if the package provides one) is the usual idiom here. Adapt the exact class names to what src/ actually exposes — the pattern is stable across Riemann clients, the namespace is not.
Wrap the client in one small service, never call it from controllers. The client is stateful (a socket) and monitoring failures must never break a request. Bind a single wrapper in the container so connection setup, error swallowing, and default fields live in one place:
final class RiemannReporter
{
public function __construct(private \Riemann\Client $client) {}
public function gauge(string $service, float|int $value, array $tags = [], ?float $ttl = null): void
{
try {
$event = new \Riemann\Proto\Event();
$event->setHost(gethostname());
$event->setService($service);
$event->setMetric((float) $value);
$event->setTtl($ttl ?? 60.0);
$event->setState('ok');
$event->setTags($tags);
$this->client->send($event);
} catch (\Throwable $e) {
report($e); // never rethrow into the request path
}
}
}
Define a naming convention before the first event ships. service is the primary key in Riemann; host + service is the identity of an event stream. Pick {app}.{component}.{measurement} (e.g. shop.checkout.queue_depth) and document it. Renaming a service later orphans history and silently breaks any dashboard or alert bound to the old name.
Send batches from scheduled collectors, single events from request paths. If you sample N metrics in an Artisan command, build all the Event objects first and send them in one sendEvents() call — one socket round trip instead of N. From inside a web request or queue worker, send at most one or two events and make it non-blocking (see Gotchas).
Use attributes for anything high-cardinality, tags for anything you filter on. Attributes are a free-form map and are the right home for tenant IDs, queue names, or build numbers. Tags are effectively a set you query with tagged "...". Never fold a dimension into the service string unless you want a separate alerting entity for it.
Adopt the conventional state vocabulary. Riemann itself does not enforce states, but every dashboard and every alert rule you will write assumes them: ok, warning, critical, and expired (which Riemann assigns itself — never send it). Reserve state for health; put the actual number in metric.
Drive it from the scheduler with a TTL longer than the interval. The canonical pattern is a metrics:collect command on a everyMinute() schedule, sending events with ttl of 90–120s. The TTL is what makes Riemann useful: if your cron dies, the event expires and you get alerted without writing a single "no data" rule.
Test against a real Riemann, but isolate the client. For unit tests, put an interface in front of the reporter (or inject a fake client) so tests assert on the events you would send — inspect the built Event objects rather than asserting on socket behaviour. For one integration test, run Riemann in Docker in CI and assert with a query.
send() means the bytes reached the socket, not that Riemann indexed them. Do not use the absence of an exception as proof of delivery, and never use Riemann itself as the source of truth for an alerting path you need to be reliable.send() in a retry-once-after-reconnect.ttl is a contract, not a decoration. Setting ttl shorter than your send interval produces a permanent expired flap that will page you at 3am. Setting it to 0/unset means the event never expires, so a dead collector looks healthy forever. Match TTL to ~1.5–2× the interval.metric nil vs 0. Protobuf optional fields distinguish "not set" from zero. If you cast carelessly ((float) null), you turn a "no measurement" into a fake zero and corrupt your graphs. Decide explicitly per service whether a missing value should suppress the event or send a state-only event.attributes over forking and regenerating.5555 too; it is lossy but non-blocking. For high-frequency, low-value metrics from a web tier, UDP is sometimes the right trade — just don't put anything alert-critical on it.nc -v localhost 5555 proves reachability, and tcpdump -i lo port 5555 proves you're actually sending. Most "the client is broken" reports are a firewall rule or a container-network mismatch.RiemannReporter wrapper is where you add default host, batching, sampling, sanitisation, and a null-object implementation for local dev. That keeps the direct dependency surface to one file.composer.json, and make sure your wrapper is small enough that swapping the underlying transport (or shelling out to riemann-client) is a one-file change.How can I help you explore Laravel packages today?