jmonitor / collector
PHP library for collecting server metrics (PHP, MySQL, Nginx, Apache, Redis, System…) and sending them to Jmonitor.io
Requires
- php: ^8.1
- composer-runtime-api: ^2.0
- ext-json: *
- ext-zlib: *
- psr/http-client: ^1.0
- psr/log: ^1 || ^2 || ^3
- symfony/process: ^6.4|^7.0|^8.0
Requires (Dev)
- ext-pdo: *
- doctrine/dbal: ^3.0|^4.2
- friendsofphp/php-cs-fixer: ^3.86
- nyholm/psr7: ^1.8
- php-http/discovery: ^1.17
- php-http/message: ^1.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.0|^11.5
- predis/predis: ^3.0
- symfony/http-client: ^6.4|^7.0|^8.0
Suggests
- ext-pdo: If you want to collect mysql datas with Pdo as driver.
- doctrine/dbal: If you want to collect mysql datas with Doctrine DBAL as driver.
- php-http/discovery: To send the metrics to jmonitor.io with whatever PSR-18 client and PSR-17 factories are installed.
- psr/http-client-implementation: To send the metrics to jmonitor.io, e.g. symfony/http-client.
- psr/http-factory-implementation: To send the metrics to jmonitor.io, e.g. nyholm/psr7.
Provides
None
Conflicts
- php-http/discovery: <1.17 || >=2.0
Replaces
None
README
This library provides the PHP collectors that gather metrics from your server and your stack, and send them to Jmonitor.
| jmonitor/jmonitor |
jmonitor/collector |
jmonitor/jmonitor-bundle |
|---|---|---|
| Self-hostable backend — not needed with the cloud version | The collectors — install them in the project to monitor | Symfony-specific integration of the collectors |
|
jmonitor/easyadmin-bundle Live monitoring pages inside EasyAdmin — no backend, no worker |
||
| https://jmonitor.io | ||
Supported components
| Category | Components |
|---|---|
| Runtime | |
| Framework | via the bundle |
| Web servers | |
| Databases & cache | |
| System | |
| CDN |
Requirements
- PHP 8.1+ (for PHP 7.4/8.0, use the 1.x branch)
- A project using Composer
Getting started
1. Create your project
Create a project on jmonitor.io — or on your self-hosted instance — and copy its API key.
2. Install the library
composer require jmonitor/collector symfony/http-client nyholm/psr7
symfony/http-client and nyholm/psr7 send the metrics to Jmonitor. Any other PSR-18 client works too: install
php-http/discovery to have it found automatically, or pass it to the Jmonitor constructor along with
nyholm/psr7. If you only read the metrics yourself (collect(send: false)), composer require jmonitor/collector
is enough.
Note
Upgrading: the HTTP client is no longer installed along with this package. If metrics stop being sent with a
MissingHttpClientException, run one of the commands above.
3. Set up your worker
The collector runs as a worker, in a separate process. This means you must not integrate it into your
application and call $jmonitor->collect() on every web request.
A ready-to-use worker ships with the package. Copy it into your project:
mkdir -p scripts && cp vendor/jmonitor/collector/scripts/worker.php scripts/worker.php
From now on it is your script: set your API key, add the collectors matching your stack (see
Collectors), and adapt it however you like — composer update will never touch it.
If you move it elsewhere, adjust the require path to vendor/autoload.php accordingly.
php scripts/worker.php
4. Run it in production
Run the worker under a process manager (Supervisor, systemd…) so it stays up and is restarted periodically. Symfony Messenger's recommendations apply as-is: https://symfony.com/doc/current/messenger.html#deploying-to-production
Some metrics are fairly static and remain cached for the lifetime of the process, so among other reasons
(memory…), it is strongly recommended to restart the worker regularly, at least once a day. The provided
worker has $timeLimitSeconds and $memoryLimitBytes options for that.
On Symfony, the bundle ships a console command that replaces this worker.
Debugging and Error Handling
Each collector is isolated and executed within a try/catch block.
Use the CollectionResult returned by collect() method to inspect outcomes.
By default, collect() call send metrics to Jmonitor.
You can disable this by passing send: false
By default, collect() do not throws when the server response status code is >= 400.
You can disable this by passing throwOnFailure: false
Finally, you can pass a PSR-3 logger to the constructor to get more detailed information about the collection process. You will receive messages ranging from debug to error level.
use Psr\Http\Message\ResponseInterface; use Jmonitor\CollectionResult; use Jmonitor\Jmonitor; $jmonitor = new Jmonitor('apiKey', logger: new SomeLogger()); /** * Send metrics, you can : * - Disable throwing an exception on error * - Disable sending metrics to the server */ $result = $jmonitor->collect(throwOnFailure: false); // Or disable completely the sending of metrics to the server $result = $jmonitor->collect(send: false); /** * Use $result to inspect */ // Human-readable summary (string) $conclusion = $result->getConclusion(); // List of Exceptions if any (\Throwable[]) $errors = $result->getErrors(); // The raw response from jmonitor, if any (ResponseInterface|null) $response = $result->getResponse(); // All metrics collected (mixed[]) $metrics = $result->getMetrics();
Collectors
-
System
Collects system metrics like CPU usage, memory usage, disk usage, etc. Linux only for now. Feel free to open an issue if you need other OS support.
use Jmonitor\Collector\System\SystemCollector; $collector = new SystemCollector(); // There is actually a "RandomAdapter" you can use on a Windows OS for testing purposes $collector = new SystemCollector(new RandomAdapter());
-
Apache
Collects metrics from Apache "mod_status" module. Enable it and expose a status URL. There are some resources to help you with that:
- Apache docs (FR - EN): https://httpd.apache.org/docs/current/mod/mod_status.html.
- Guide (EN): https://statuslist.app/apache/apache-status-page-simple-setup-guide/
- Guide (FR): https://www.blog.florian-bogey.fr/activer-et-configurer-le-server-status-apache-mod_status.html
use Jmonitor\Collector\Apache\ApacheCollector; $collector = new ApacheCollector('http://localhost/server-status');
Traffic metrics (accesses, bytes, request durations) are only reported by mod_status with
ExtendedStatus On, the default when mod_status is loaded: do not turn it off. -
Nginx
Collects metrics from Nginx "stub_status" module. Enable it and expose a status URL. There are some resources to help you with that:
- Nginx docs (EN): https://nginx.org/en/docs/http/ngx_http_stub_status_module.html
- Stackoverflow (EN): https://stackoverflow.com/questions/62269902/nginx-how-to-create-status-with-stub-status
- Guide (EN): https://easyengine.io/tutorials/nginx/status-page/
use Jmonitor\Collector\Nginx\NginxCollector; $collector = new NginxCollector('http://localhost/nginx_status');
The configuration is read with
nginx -T, run by the PHP user. When that user is not root and the master process is (e.g.www-dataon Debian),nginx -Tfails on the pid file (open() "/run/nginx.pid" failed (13: Permission denied)) and theconfigmetrics stay empty. -
Mysql
Collects MySQL metrics from variables, status, and the
performance_schemaandinformation_schematables if availables.
Connect via PDO or Doctrine DBAL (open an issue if you need other drivers, e.g., mysqli).use Jmonitor\Collector\Mysql\MysqlCollector; use Jmonitor\Collector\Mysql\Adapter\PdoAdapter; use Jmonitor\Collector\Mysql\Adapter\DoctrineAdapter; use Jmonitor\Collector\Mysql\MysqlStatusCollector; use Jmonitor\Collector\Mysql\MysqlVariablesCollector; use Jmonitor\Collector\Mysql\MysqlInformationSchemaCollector use Jmonitor\Collector\Mysql\MysqlSlowQueriesCollector // Using PDO $adapter = new PdoAdapter($pdo); // your \PDO instance // or using Doctrine DBAL $adapter = new DoctrineAdapter($connection); // your Doctrine\DBAL\Connection instance // Mysql has multiple collectors, use the same adapter for all of them $collector = new MysqlInformationSchemaCollector($adapter, 'your_db_name'); $collector = new MysqlSlowQueriesCollector($adapter, 'your_db_name'); $collector = new MysqlStatusCollector($adapter); $collector = new MysqlVariablesCollector($adapter); // The slow queries collector can be configured to filter results: // - limit: maximum number of results to return (1–10, default: 5) // - minExecCount: minimum number of executions required to include a query (default: 1) // - minAvgTimeMs: minimum average execution time (in ms) for a query to be considered slow (default: 0) // - orderBy: column used for sorting; allowed values are "sum", "avg", "max" // (see constants in MysqlSlowQueriesCollector, default: "avg") $collector = new MysqlSlowQueriesCollector($adapter, 'your_db_name', $limit, $minExecCount, $minAvgTimeMs, $orderBy);
-
PHP
Collects PHP metrics (loaded extensions, some ini keys, FPM, opcache, etc.).
Important
PHP configuration can differ significantly between CLI and web server.
To collect web‑server context metrics from a CLI script, which is probably what you want to do, expose an HTTP endpoint that returns these metrics as JSON (see below).
-
Collect CLI-context metrics (current context)
use Jmonitor\Collector\Php\PhpCollector; $collector = new PhpCollector();
-
Collect web-server context metrics from CLI
Copy the exposer script shipped with the package into a publicly reachable directory, and make sure it is properly secured:cp vendor/jmonitor/collector/scripts/php-exposer.php public/php-metrics.php
It is a plain PHP file echoing the metrics as JSON — it is yours too, adapt it if needed.
Then, in your CLI script, point the collector to its URL:
use Jmonitor\Collector\Php\PhpCollector; $collector = new PhpCollector('https://localhost/php-metrics.php');
-
Redis
Collects Redis metrics from the INFO command.
use Jmonitor\Collector\Redis\RedisCollector; // Any client supporting INFO: PhpRedis, Predis, RedisArray, RedisCluster, Relay... $redis = new \Redis([...]); $collector = new RedisCollector($redis);
-
Caddy
Collects from the Caddy metrics endpoint. See below if you also use FrankenPhp.
use Jmonitor\Collector\Caddy\CaddyCollector use Jmonitor\Prometheus\PrometheusMetricsProvider; $collector = new CaddyCollector(new PrometheusMetricsProvider('http://localhost:2019/metrics'));
-
FrankenPHP
Collects from the Caddy metrics endpoint of FrankenPHP. You must reuse the PrometheusMetricsProvider instance for both collectors to avoid an unnecessary extra HTTP request.
use Jmonitor\Collector\Caddy\CaddyCollector use Jmonitor\Prometheus\PrometheusMetricsProvider; use Jmonitor\Collector\FrankenPhp\FrankenPhpCollector; $metricsProvider = new PrometheusMetricsProvider('http://localhost:2019/metrics'); $caddyCollector = new CaddyCollector($metricsProvider); $frankenPhpCollector = new FrankenPhpCollector($metricsProvider);
-
PostgreSQL
Collects PostgreSQL metrics from system catalog views (
pg_stat_database,pg_stat_activity,pg_stat_bgwriter, etc.).
Connect via PDO or Doctrine DBAL.use Jmonitor\Collector\Postgresql\PostgresqlActivityCollector; use Jmonitor\Collector\Postgresql\PostgresqlDatabaseCollector; use Jmonitor\Collector\Postgresql\PostgresqlSettingsCollector; use Jmonitor\Collector\Postgresql\PostgresqlSlowQueriesCollector; use Jmonitor\Utils\DatabaseAdapter\PdoAdapter; use Jmonitor\Utils\DatabaseAdapter\DoctrineAdapter; // Using PDO $adapter = new PdoAdapter($pdo); // your \PDO instance // or using Doctrine DBAL $adapter = new DoctrineAdapter($connection); // your Doctrine\DBAL\Connection instance // PostgreSQL has multiple collectors; use the same adapter for all of them $collector = new PostgresqlActivityCollector($adapter); $collector = new PostgresqlSettingsCollector($adapter); $collector = new PostgresqlDatabaseCollector($adapter); // defaults to schema 'public' $collector = new PostgresqlDatabaseCollector($adapter, 'my_schema'); // custom schema $collector = new PostgresqlSlowQueriesCollector($adapter);
PostgresqlSlowQueriesCollectorrequires thepg_stat_statementsextension.
Add it toshared_preload_librariesinpostgresql.confand restart PostgreSQL, then run:CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Pass
autoCreateExtension: trueto let the collector create it automatically (requires superuser or CREATE privileges):// The slow queries collector can be configured: // - limit: maximum number of results (default: 10) // - minCalls: minimum number of executions to include a query (default: 1) // - minMeanTimeMs: minimum average execution time in ms (default: 0) // - orderBy: "avg", "total", or "max" (see constants in PostgresqlSlowQueriesCollector, default: "avg") // - autoCreateExtension: create pg_stat_statements if missing (default: false) $collector = new PostgresqlSlowQueriesCollector($adapter, limit: 10, minCalls: 5, minMeanTimeMs: 100.0, orderBy: PostgresqlSlowQueriesCollector::ORDER_BY_AVG_TIME);
-
Cloudflare
Collects, for one Cloudflare zone, what your server cannot see about itself: what Cloudflare absorbs (cache, security), the origin latency seen from the edge, real-user experience (Web Analytics, Core Web Vitals), and the state and usage of R2 and Images. It queries the Cloudflare REST and GraphQL Analytics APIs and works in full on a Free plan.
use Jmonitor\Collector\Cloudflare\CloudflareCollector; $collector = new CloudflareCollector( apiToken: getenv('CLOUDFLARE_API_TOKEN') ?: '', zone: 'example.com', // zone name or zone id httpClient: null, // optional PSR-18 client, discovered when null options: [ 'refresh' => [ // seconds, these are the defaults 'traffic' => 60, 'today' => 300, 'web_analytics' => 300, 'storage' => 900, // r2 and images 'zone' => 3600, ], 'lag' => 180, // seconds: the data window ends at the start of the minute of now - lag 'lookback' => 5, // minutes of per-minute traffic re-read on every refresh (60 at most) 'window' => 15, // minutes covered by the traffic breakdowns ], );
Create an API token in the Cloudflare dashboard (My Profile → API Tokens) with these permissions:
Permission Needed for Zone › Zone › Read required: zone lookup, zoneblockZone › Zone Settings › Read zone.settingsZone › Analytics › Read traffic,todayZone › SSL and Certificates › Read zone.certificateZone › DNS › Read zone.dns_recordsAccount › Account Settings › Read web_analytics(site lookup)Account › Account Analytics › Read GraphQL queries of web_analytics,r2,imagesAccount › Workers R2 Storage › Read r2Account › Images › Read images.storedOnly Zone › Zone › Read is required. A block whose permission is missing is reported as
forbiddenand the others keep working; a missingzone.*permission only drops that sub-key from thezoneblock. An empty, malformed or rejected token, or an empty or unknown zone, fails the collector at startup: it is skipped and the other collectors keep running. A network error or a Cloudflare outage at startup does not, the zone lookup is retried a minute later.Cloudflare is not queried on every collection: each block is refreshed on its own interval (above) and served from memory in between, which keeps the token well under the Cloudflare API rate limits.
The
web_analyticsblock needs both Web Analytics enabled on the zone (Cloudflare dashboard → Web Analytics) and the Account › Account Settings › Read permission, which the site lookup requires: Account Analytics Read alone is not enough. Workers, Pages, KV, D1 and DNS analytics are left out on purpose.When no client is passed and
symfony/http-clientis installed, the collector builds one with a 5-second timeout. PSR-18 has no timeout API, so configure one on any client you pass yourself.
Need help?
- Anything about this package — installation, the worker, a collector, a missing or wrong metric: open an issue on this repo https://github.com/jmonitor/collector/issues
- Anything about the app itself — dashboards, alerts, dash.jmonitor.io: open an issue on https://github.com/jmonitor/jmonitor/issues
