Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Prometheus Client Php Laravel Package

jimdo/prometheus_client_php

PHP Prometheus client library for exposing counters, gauges, and histograms, with Redis, APCu, or in-memory client-side aggregation and text-format rendering. Note: development has moved to endclothing/prometheus_client_php.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install — but not this package. The Jimdo repo is archived and explicitly redirects to the maintained fork. Use that one; the API is a superset and PHP 8 compatible:

composer require promphp/prometheus_client_php

Only pull jimdo/prometheus_client_php if you're pinning an existing legacy app (PHP ≤ 7.4, no PHP 8.1+ deprecation noise).

2. Pick an adapter before writing any metric code. This is the single decision that shapes everything else:

Adapter Use when
Prometheus\Storage\Redis Default for web apps. Shared across FPM workers and across hosts. Supports Summary.
Prometheus\Storage\APCng / APC Single-host, APC(u) installed, no Redis. Per-pool only — breaks behind a load balancer.
Prometheus\Storage\InMemory Unit tests and long-running CLI daemons (queue workers, cron loops). Useless in FPM: state dies with the request.

Run a Redis on the same box/socket as the PHP workers — every metric write is a round-trip, so a cross-AZ Redis will show up in your p99.

3. Register the registry once, in a container binding (Laravel example):

// app/Providers/PrometheusServiceProvider.php
$this->app->singleton(\Prometheus\Storage\Adapter::class, function () {
    return new \Prometheus\Storage\Redis([
        'host' => config('prometheus.redis.host', '127.0.0.1'),
        'port' => config('prometheus.redis.port', 6379),
        'timeout' => 0.1,
        'read_timeout' => 10,
        'persistent_connections' => false,
        'prefix' => config('prometheus.redis.prefix', 'PROMETHEUS_'),
    ]);
});

$this->app->singleton(\Prometheus\CollectorRegistry::class, fn ($app) =>
    new \Prometheus\CollectorRegistry($app->make(\Prometheus\Storage\Adapter::class))
);

4. Expose the scrape endpoint. The library does not ship HTTP handling — that's on you:

Route::get('/metrics', function (\Prometheus\CollectorRegistry $registry) {
    $renderer = new \Prometheus\RenderTextFormat();
    return response(
        $renderer->render($registry->getMetricFamilySamples()),
        200,
        ['Content-Type' => \Prometheus\RenderTextFormat::MIME_TYPE] // text/plain; version=0.0.4
    );
})->withoutMiddleware(['web', 'auth']); // keep it off the session/CSRF stack

5. First real use case — one counter in middleware. Increment on every request, keyed by route template and status class:

$counter = $registry->getOrRegisterCounter(
    'app', 'http_requests', 'HTTP requests', ['route', 'method', 'status']
);
$counter->inc(['users.show', 'GET', '2xx']);

Where to look first: src/Prometheus/CollectorRegistry.php (the getOrRegister* contract), src/Prometheus/Storage/Redis.php (key layout, prefix, Lua scripts), and src/Prometheus/RenderTextFormat.php (what actually hits the wire). Read the MetricFamilySamples output once with curl localhost/metrics | head -50 before you trust any dashboard.

Implementation Patterns

RED metrics via middleware. Wrap the request lifecycle; use the route template ($request->route()->getName() or the compiled URI pattern), never $request->path(), or you'll mint a label value per user ID:

public function handle($request, Closure $next)
{
    $timer = $this->histogram->startTimer([$route, $request->method()]);
    $response = $next($request);
    $timer->observeDuration();
    $this->counter->inc([$route, $request->method(), substr((string) $response->getStatusCode(), 0, 1).'xx']);
    return $response;
}

Register the histogram with buckets tuned to your SLO, not the defaults (0.005…10.0) — 13 buckets × N label combos is 13N Redis sorted sets:

$registry->getOrRegisterHistogram(
    'app', 'http_request_duration_seconds', 'Duration', ['route', 'method'],
    [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
);

Wrap it in a small facade/service, not raw registry calls. You want one place that owns namespaces, label ordering, and metric definitions, so a rename is a one-line change and tests can swap in InMemory:

class Metrics
{
    public function __construct(private CollectorRegistry $registry) {}

    public function orderPlaced(int $cents, string $channel): void
    {
        $this->registry->getOrRegisterCounter('app', 'orders_placed', 'Orders placed', ['channel'])
            ->inc([$channel]);
        $this->registry->getOrRegisterHistogram('app', 'order_value_cents', 'Order value', [], [1000, 5000, 10000, 50000])
            ->observe($cents);
    }
}

Call it from domain services/events, not controllers — business metrics belong next to the business logic.

Gauges for "current state" only. Queue depth, cache entries, active connections. Set them from a scheduled job that queries the real source, or from the worker itself. Never use a gauge for anything cumulative — resets and multi-worker overwrites will produce sawtooth garbage. Gauges don't sum across workers: each set() is the last write wins.

Long-running workers and cron. The InMemory adapter is genuinely useful here — a queue worker holds counters in its own process and the values are flushed... which means nothing scrapes them. Two real options:

  • Run a tiny HTTP server inside the worker (Swoole/RoadRunner/ReactPHP) exposing /metrics from InMemory, one port per worker.
  • Push a rendered payload to a Pushgateway on shutdown/interval. The library has no push client, so you render and POST:
$body = (new RenderTextFormat())->render($registry->getMetricFamilySamples());
Http::withBody($body, RenderTextFormat::MIME_TYPE)
    ->post(config('prometheus.pushgateway').'/metrics/job/'.$jobName);

Remember to DELETE the job key on start, or you'll scrape stale values after a crash.

Testing. Bind InMemory in the test container and assert on rendered output — that's the only assertion surface the library gives you:

$adapter = new \Prometheus\Storage\InMemory();
$registry = new \Prometheus\CollectorRegistry($adapter);
// ... exercise code ...
$text = (new \Prometheus\RenderTextFormat())->render($registry->getMetricFamilySamples());
$this->assertStringContainsString('app_orders_placed{channel="web"} 1', $text);
$adapter->wipeStorage(); // between tests

Naming conventions for dashboards that survive. Namespace = app or subsystem (app, billing, worker). Unit suffix on the metric name (_seconds, _bytes, _total). Keep the cardinality budget written down somewhere: every label value combination costs a Redis key and a series in Prometheus.

Gotchas and Tips

This repo is archived. Bug reports and PHP 8 fixes go nowhere. Migrate to endclothing/prometheus_client_php (Packagist promphp/prometheus_client_php) — the CollectorRegistry::getDefault() static is deprecated there in favour of constructor injection, so a container binding written today survives the swap.

CollectorRegistry::getDefault() is global mutable state. Convenient in the README, painful in tests (the registry persists across test methods within a process) and impossible to reconfigure per-environment. Always construct explicitly and bind it as a singleton.

getOrRegister* throws on re-registration with a different signature. MetricsRegistrationException fires if the same name comes back with different label names or bucket bounds. During a rolling deploy where you add a label, old and new workers disagree and requests start 500ing. Sequence it: deploy the new label set under a new metric name, backfill dashboards, then delete the old one. There is no atomic rename.

Label values must be a positional list. They're zipped against the registered label names by index — an associative array's keys are ignored, so ['status' => '200'] silently mislabels. Always pass array_values()-ordered scalars, and make a helper that enforces the order.

Never let unbounded values into labels. User IDs, raw URLs, exception messages, SQL strings, full IPs, session IDs. One bad label can take down the scrape (Prometheus will refuse or the target will OOM on series churn). Cap it: route templates, status classes, error types.

Metric/label name validation is strict. [a-zA-Z_:][a-zA-Z0-9_:]* for names, label names can't start with __, and le / quantile are reserved. Namespacing uses _ as the joiner, so a namespace ending in _ gives you a double underscore. Invalid names throw at registration time, not at scrape time — which is better, but only if the code path actually runs, so test the cold path.

Counter naming and _total. Prometheus convention is foo_total. Depending on version, this library either appends _total for you or exposes the name verbatim — check your actual /metrics output before naming an alert, or you'll write increase(foo_total[5m]) against a series called foo.

Summary metrics are effectively Redis-only. Quantile computation needs time-windowed storage that InMemory and APC don't provide. If you're on APC and need quantiles, either switch adapters or use histograms + histogram_quantile() in PromQL.

APCu and CLI don't mix by default. apc.enable_cli=0 means every artisan command writes to a throwaway cache. Set apc.enable_cli=1 or your cron metrics disappear. Also: APC is per-host, so a 3-node app behind an LB only exposes one third of your traffic to each scrape — either scrape each node separately (honor_labels/target per host) or don't use APC.

Redis adapter specifics. Default key prefix is PROMETHEUS_ — change it if the Redis instance is shared, or wipeStorage() in a test will nuke someone else's keys. Keys look like PROMETHEUS_:counter:app:http_requests. Histogram observes and Summary observes go through Lua EVAL, so a Redis ACL that disallows EVAL breaks them with a confusing error. Keep timeout low (~100ms) — metric writes should never be the reason a request hangs.

Each getOrRegister* call is a storage read. It's cheap but not free; don't call it inside a tight loop. Resolve metrics once (constructor or request-scope cache), then inc()/observe() many times.

The /metrics route is a load-test and an information leak. It's unauthenticated by design (Prometheus can't do OAuth easily), so bind it to a private interface, allowlist the scraper IP, or use a sidecar. Expose it via a dedicated nginx location that bypasses PHP-FPM if the metric set is static. And exclude it from your own RED middleware or you'll double-count scrapes.

Empty output is the most common surprise. A blank /metrics body means the metrics were registered in a different worker (APC), the prefix changed, or getMetricFamilySamples() returned nothing because registration happened on a code path that didn't execute that request. Redis debugging:

redis-cli --scan --pattern 'PROMETHEUS_*' | head
redis-cli type 'PROMETHEUS_:counter:app:http_requests'
redis-cli hgetall 'PROMETHEUS_:counter:app:http_requests'

Validate the exposition format before you page anyone. promtool check metrics < /tmp/metrics.txt catches duplicate series, bad names, and malformed help text — all things the renderer will happily emit.

PHP 8.1+ on the Jimdo release produces deprecation notices from internal strlen(null)-style calls; if warnings get promoted to exceptions by your error handler, metric recording becomes a 500. Another reason to take the fork.

Extension point if you need custom storage: implement Prometheus\Storage\Adapter (write, update, wipeStorage, collect). Useful for a PDO-backed or memcached-backed store, but you own atomicity — the built-in adapters get it from Redis Lua / apcu_inc.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route