mathiasgrimm / netwatch
Network service latency probing tool for PHP — measures Redis, PostgreSQL, MySQL, S3, HTTP endpoints with statistical analysis
Requires
- php: ^8.3
- illuminate/http: ^10.0 || ^11.0 || ^12.0 || ^13.0
- symfony/console: ^6.0 || ^7.0 || ^8.0
- symfony/process: ^6.0 || ^7.0 || ^8.0
Requires (Dev)
- laravel/framework: ^12.54
- laravel/pint: ^1.29
- mockery/mockery: ^1.6
- orchestra/testbench-core: ^10.9
- pestphp/pest: ^4.4
Suggests
- illuminate/support: Required for Laravel integration (^10.0 || ^11.0 || ^12.0)
This package is auto-updated.
Last update: 2026-07-21 07:40:42 UTC
README
Netwatch
Know your latency before your users do. Statistical network probing for Redis, databases, S3, and HTTP - from the CLI or inside Laravel.
When an application feels slow, the first question is always the same: is it the app, or is it the network? Answering it usually means SSH-ing into a box and juggling ping, redis-cli --latency, psql, and curl by hand - with no consistent numbers to compare across environments.
Netwatch answers it in one command. It probes each of your services - Redis, MySQL, PostgreSQL, S3, HTTP endpoints, raw TCP - for N iterations, splits every measurement into connect and request time, and reports min/max/avg and p50/p95/p99 percentiles. Run it as a standalone CLI in any PHP project, or drop it into Laravel and get an Artisan command plus a health dashboard preconfigured from your existing .env.
Features
- Multiple probe types - HTTP, TCP/IP, Redis (php-redis), MySQL/PostgreSQL/SQLite (PDO), AWS S3
- Statistical analysis - min, max, avg, p50, p95, p99 for connect, request, and total latency
- Latency thresholds - per-probe warn/crit limits drive the dashboard, JSON API and CLI alike, with
--fail-on-critfor cron/CI - Parallel execution - probes run concurrently via subprocesses (default in CLI)
- Per-probe configuration - individual iteration counts, enable/disable flags, serializable probe definitions
- Laravel integration - service provider with auto-discovery, Artisan command, and health dashboard
- Standalone CLI - works with any PHP project via Symfony Console
- Fail-fast - stops probing after 3 consecutive failures
- Custom probes - implement a single interface to measure anything
Requirements
- PHP 8.3+
ext-curl(for HTTP and S3 probes)ext-redis(for Redis probe, optional)ext-pdo(for database probes, optional)
Installation
composer require mathiasgrimm/netwatch
Laravel Integration
Netwatch auto-registers via Laravel package discovery. No manual provider registration needed.
Install
php artisan netwatch:install
This publishes the config file and service provider, and registers the provider in bootstrap/providers.php.
To overwrite previously published files:
php artisan netwatch:install --force
You can also publish assets individually:
php artisan vendor:publish --tag=netwatch-config php artisan vendor:publish --tag=netwatch-provider
The config file (config/netwatch.php) contains pre-configured probes that read from your existing Laravel environment variables (DB_*, REDIS_*, AWS_*, APP_URL). The config uses the serializable [Class::class => [args]] array format, so it is fully compatible with php artisan config:cache.
<?php use MathiasGrimm\Netwatch\Laravel\Http\Middleware\Authorize; use MathiasGrimm\Netwatch\Probe\HttpProbe; use MathiasGrimm\Netwatch\Probe\PdoProbe; use MathiasGrimm\Netwatch\Probe\PhpRedisProbe; use MathiasGrimm\Netwatch\Probe\S3Probe; use MathiasGrimm\Netwatch\Probe\TcpPingProbe; return [ 'iterations' => (int) env('NETWATCH_ITERATIONS', 10), 'health_route' => [ 'enabled' => (bool) env('NETWATCH_HEALTH_ENABLED', false), 'domain' => env('NETWATCH_DOMAIN'), 'path' => env('NETWATCH_PATH', 'netwatch'), 'middleware' => ['web', Authorize::class], 'token' => env('NETWATCH_HEALTH_TOKEN'), ], 'probes' => [ 'database' => [ 'enabled' => env('NETWATCH_PROBE_DATABASE_ENABLED', false), 'probe' => [ PdoProbe::class => [ env('DB_CONNECTION').':host='.env('DB_HOST').';port='.env('DB_PORT').';dbname='.env('DB_DATABASE'), env('DB_USERNAME'), env('DB_PASSWORD'), ], ], ], 'database-tcp' => [ 'enabled' => env('NETWATCH_PROBE_DATABASE_TCP_ENABLED', false), 'probe' => [ TcpPingProbe::class => [ parse_url((string) env('DB_HOST'), PHP_URL_HOST) ?: env('DB_HOST'), (int) (env('DB_PORT') ?: match (env('DB_CONNECTION')) { 'pgsql' => 5432, 'sqlsrv' => 1433, default => 3306, }), ], ], ], 'redis' => [ 'enabled' => env('NETWATCH_PROBE_REDIS_ENABLED', false), 'probe' => [ PhpRedisProbe::class => [ env('REDIS_HOST').':'.env('REDIS_PORT'), env('REDIS_USERNAME'), env('REDIS_PASSWORD'), ], ], ], 'redis-tcp' => [ 'enabled' => env('NETWATCH_PROBE_REDIS_TCP_ENABLED', false), 'probe' => [ TcpPingProbe::class => [ parse_url((string) env('REDIS_HOST'), PHP_URL_HOST) ?: env('REDIS_HOST'), (int) (env('REDIS_PORT') ?: 6379), ], ], ], 's3' => [ 'enabled' => env('NETWATCH_PROBE_S3_ENABLED', false), 'probe' => [ S3Probe::class => [ env('AWS_BUCKET'), env('AWS_DEFAULT_REGION'), env('AWS_ACCESS_KEY_ID'), env('AWS_SECRET_ACCESS_KEY'), env('AWS_ENDPOINT'), ], ], ], 'app' => [ 'enabled' => env('NETWATCH_PROBE_APP_ENABLED', false), 'probe' => [ HttpProbe::class => [ env('APP_URL'), ], ], ], 'cloudflare-dns' => [ 'enabled' => env('NETWATCH_PROBE_CLOUDFLARE_DNS_ENABLED', false), 'probe' => [ TcpPingProbe::class => [ '1.1.1.1', 53, ], ], ], 'google-dns' => [ 'enabled' => env('NETWATCH_PROBE_GOOGLE_DNS_ENABLED', false), 'probe' => [ TcpPingProbe::class => [ '8.8.8.8', 53, ], ], ], ], ];
Artisan Command
php artisan netwatch:run php artisan netwatch:run --probe=redis --iterations=20 --json php artisan netwatch:run --json --without-results
| Option | Description |
|---|---|
--iterations=N |
Override iteration count for all probes |
--probe=NAME |
Run only a specific probe by name (e.g. --probe=redis) |
--json |
Output results as JSON instead of a table |
--without-results |
Exclude individual iteration results from JSON output |
--fail-on-crit |
Exit non-zero when any probe fails or breaches its crit latency threshold |
Health Dashboard
Enable the health dashboard route by setting NETWATCH_HEALTH_ENABLED=true in your .env. Once enabled, access is restricted to the local environment until you register your own gate or token (see Authorization), so it is safe to leave on in production:
NETWATCH_HEALTH_ENABLED=true NETWATCH_PATH=netwatch
Access the dashboard at /netwatch/health.
The HTML dashboard renders instantly and loads each probe asynchronously: every enabled probe appears immediately as a placeholder card, then its results stream in from GET /netwatch/health/probes/{name} (one concurrent request per probe, same auth as /health). Use the Auto 30s toolbar toggle to keep re-running the probes every 30 seconds while the tab is open (off by default, remembered per browser, paused while the tab is in the background).
Cards accumulate results across refreshes: the sparkline grows run by run (thin separators mark each run, capped at the last 60 iterations), the stats table and headline p95 are computed over the full accumulated history, and the headline shows a p95 trend versus the previous run. When a run completes, cards re-sort by severity (failing first, then over-threshold, then healthy) and the header shows a summary like 1 failing · 2 slow · 3 healthy.
Latency thresholds — each probe has optional warn/crit thresholds in milliseconds. The probe-level status compares total p95 against them (breaching warn turns the headline amber, crit red), and every individual iteration is checked too: over-threshold samples color their sparkline bars amber/red, dashed guide lines mark the thresholds on the chart, and the JSON payload carries a status per sample plus over_warn/over_crit counts. Defaults ship for the built-in probes (see Environment Variables below); set any threshold env to empty to disable it. The same thresholds drive the dashboard, the JSON API, and the CLI.
The JSON API (?format=json or an application/json Accept header) is unaffected: it still runs all requested probes synchronously and returns the complete result set in one response.
Upgrading? If you previously published the views (
--tag=netwatch-views), re-publish them: the dashboard now requires the async view and the newpartials/card.blade.php. A previously publishedconfig/netwatch.phpkeeps working: built-in probes whose config has nothresholdskey fall back to the package defaults (still tunable via the*_WARN_MS/*_CRIT_MSenv vars). Setting'thresholds' => null— or any individual value to null/empty — explicitly disables the threshold.
Query Parameters
| Parameter | Example | Description |
|---|---|---|
format |
?format=json |
Force response format: json or html. Without this parameter, the format is determined by the Accept header (defaults to HTML for browsers). |
probes |
?probes=redis,database |
Comma-separated list of probe names to run. Only the specified probes are executed; others are skipped. |
without_results |
?without_results=1 |
Exclude individual iteration results from the response, returning only aggregate stats. Useful for smaller payloads. |
Each probe in the JSON response includes, alongside stats, failures and results: a status (ok / warn / crit / failing, evaluated from the latency thresholds), the resolved thresholds, over_warn/over_crit sample counts, and a per-iteration status on every entry in results.
Parameters can be combined: /netwatch/health?probes=redis,database&format=json&without_results=1
HTML view - interactive dashboard with per-probe latency stats, in dark and light themes:
Export image - download a branded image summary of the current results (status, sparkline, and connect/request/total latency stats per probe) straight from the dashboard toolbar. Exports as WebP (~150 KB), falling back to PNG in browsers without WebP encoding:
JSON panel - view raw JSON data directly within the dashboard:
JSON API response - append ?format=json for a raw JSON endpoint:
Authorization
Token-based auth (for monitoring tools)
Monitoring tools (Datadog, uptime checkers, etc.) that can't use session-based auth can authenticate with a query-parameter token:
NETWATCH_HEALTH_TOKEN=your-secret-token
Then access the endpoint as /netwatch/health?token=your-secret-token. When a valid token is provided, session/gate-based auth is bypassed. If the token is not set, regular auth applies unchanged.
Gate-based auth
By default, the health route is accessible only in the local environment: Netwatch registers a default gate that returns true only when app()->environment('local'), and the Authorize middleware denies the request (403) whenever no gate returns true and no valid token is supplied. To grant access in other environments, register your own gate, which replaces the default. Publish the service provider:
php artisan vendor:publish --tag=netwatch-provider
Then edit app/Providers/NetwatchServiceProvider.php:
protected function gate(): void { Gate::define('viewNetwatch', function ($user = null) { return in_array(optional($user)->email, [ 'admin@example.com', ]); }); }
You can also use the static auth callback:
Netwatch::auth(function ($request) { return $request->user()?->isAdmin(); });
Environment Variables
NETWATCH_ITERATIONS=10 # Default probe iterations NETWATCH_HEALTH_ENABLED=false # Enable health dashboard route NETWATCH_DOMAIN=null # Domain for health route NETWATCH_PATH=netwatch # URL path prefix for health route NETWATCH_HEALTH_TOKEN=null # Token for monitoring-tool access (null = disabled) NETWATCH_PROBE_DATABASE_ENABLED=false # Enable database (PDO) probe NETWATCH_PROBE_DATABASE_TCP_ENABLED=false # Enable raw TCP connect probe to DB_HOST:DB_PORT NETWATCH_PROBE_REDIS_ENABLED=false # Enable Redis probe NETWATCH_PROBE_REDIS_TCP_ENABLED=false # Enable raw TCP connect probe to REDIS_HOST:REDIS_PORT NETWATCH_PROBE_S3_ENABLED=false # Enable S3 probe NETWATCH_PROBE_APP_ENABLED=false # Enable HTTP probe for APP_URL NETWATCH_PROBE_CLOUDFLARE_DNS_ENABLED=false # Enable Cloudflare DNS (1.1.1.1) TCP probe NETWATCH_PROBE_GOOGLE_DNS_ENABLED=false # Enable Google DNS (8.8.8.8) TCP probe NETWATCH_PROBE_DATABASE_WARN_MS=50 # Latency thresholds (total p95, ms) per probe. NETWATCH_PROBE_DATABASE_CRIT_MS=100 # warn = amber, crit = red on the dashboard. NETWATCH_PROBE_DATABASE_TCP_WARN_MS=10 # Set any of them to null/empty to disable NETWATCH_PROBE_DATABASE_TCP_CRIT_MS=25 # that threshold. NETWATCH_PROBE_REDIS_WARN_MS=50 NETWATCH_PROBE_REDIS_CRIT_MS=100 NETWATCH_PROBE_REDIS_TCP_WARN_MS=5 NETWATCH_PROBE_REDIS_TCP_CRIT_MS=25 NETWATCH_PROBE_S3_WARN_MS=150 NETWATCH_PROBE_S3_CRIT_MS=500 NETWATCH_PROBE_APP_WARN_MS=500 NETWATCH_PROBE_APP_CRIT_MS=1000 NETWATCH_PROBE_CLOUDFLARE_DNS_WARN_MS=35 NETWATCH_PROBE_CLOUDFLARE_DNS_CRIT_MS=50 NETWATCH_PROBE_GOOGLE_DNS_WARN_MS=35 NETWATCH_PROBE_GOOGLE_DNS_CRIT_MS=50
Standalone Usage
CLI
Generate a config file and run:
# Generate config vendor/bin/netwatch netwatch:init # Run all probes vendor/bin/netwatch netwatch:run # Run a specific probe with 20 iterations vendor/bin/netwatch netwatch:run --probe redis --iterations 20 # JSON output vendor/bin/netwatch netwatch:run --json # Exit non-zero when any probe fails or breaches its crit threshold (cron/CI) vendor/bin/netwatch netwatch:run --fail-on-crit
The table shows a per-probe Status column and colors the total p95 against the probe's latency threshold (amber over warn, red over crit) — the same thresholds the dashboard and JSON API use. netwatch:run inside Laravel (artisan) supports the same options.
Programmatic
use MathiasGrimm\Netwatch\Netwatch; use MathiasGrimm\Netwatch\Probe\HttpProbe; use MathiasGrimm\Netwatch\Probe\TcpPingProbe; $netwatch = new Netwatch([ 'example' => ['probe' => new HttpProbe('https://example.com')], 'dns' => ['probe' => new TcpPingProbe('8.8.8.8', 53)], ], iterations: 10); $results = $netwatch->run(); foreach ($results as $name => $aggregate) { echo "$name: avg={$aggregate->stats['total_ms']['avg']}ms p99={$aggregate->stats['total_ms']['p99']}ms\n"; }
Configuration
Standalone Config File
Create a netwatch.php in your project root (or use netwatch:init to generate one):
<?php use MathiasGrimm\Netwatch\Probe\PhpRedisProbe; use MathiasGrimm\Netwatch\Probe\PdoProbe; use MathiasGrimm\Netwatch\Probe\HttpProbe; use MathiasGrimm\Netwatch\Probe\TcpPingProbe; return [ 'iterations' => 10, 'probes' => [ 'redis' => [ 'enabled' => true, 'probe' => new PhpRedisProbe('tcp://127.0.0.1:6379'), ], 'mysql' => [ 'enabled' => true, 'probe' => new PdoProbe('mysql:host=127.0.0.1;port=3306', 'root', ''), ], 'pgsql' => [ 'enabled' => true, 'probe' => new PdoProbe('pgsql:host=127.0.0.1;port=5432;dbname=postgres', 'postgres', ''), ], 'app' => [ 'enabled' => true, 'probe' => new HttpProbe('https://example.com'), ], 'cloudflare' => [ 'enabled' => true, 'probe' => new TcpPingProbe('1.1.1.1', 443), ], ], ];
Per-Probe Options
Each probe entry supports:
| Key | Type | Default | Description |
|---|---|---|---|
probe |
ProbeInterface|string|array |
required | Probe instance, class string, or [Class::class => [args]] array |
enabled |
bool |
false |
Probe is skipped unless explicitly set to true |
iterations |
int |
global default | Override iteration count for this probe |
'redis' => [ 'enabled' => true, 'probe' => new PhpRedisProbe('tcp://127.0.0.1:6379'), 'iterations' => 20, ],
The probe value supports three formats:
- Instance - a
ProbeInterfaceobject, used as-is - Class string - e.g.
PhpRedisProbe::class, instantiated with no arguments - Array -
[Class::class => [arg1, arg2, ...]], instantiated with the given arguments. This format is serializable, making it compatible withphp artisan config:cache.
// Array format (serializable) 'database' => [ 'enabled' => true, 'probe' => [ PdoProbe::class => ['mysql:host=127.0.0.1;port=3306', 'root', ''], ], ],
Available Probes
HttpProbe
Measures HTTP endpoint latency using cURL. Reports connect time and request time separately.
new HttpProbe( url: 'https://example.com', method: 'GET', // HTTP method (default: GET) headers: [], // Custom headers timeout: 3.0, // Timeout in seconds expectedCode: null, // Expected HTTP status code (null = 200-399) )
TcpPingProbe
Measures raw TCP connection latency via fsockopen.
new TcpPingProbe( host: '8.8.8.8', port: 53, timeout: 3.0, )
PdoProbe
Measures database connectivity by opening a PDO connection and running SELECT 1.
new PdoProbe( dsn: 'mysql:host=127.0.0.1;port=3306;dbname=mydb', username: 'root', password: 'secret', timeout: 3, )
Supports any PDO driver: MySQL, PostgreSQL, SQLite, etc.
PhpRedisProbe
Measures Redis latency using the php-redis extension. Connects, authenticates, and runs PING.
new PhpRedisProbe( address: 'tcp://127.0.0.1:6379', username: null, password: null, timeout: 3.0, )
S3Probe
Measures AWS S3 bucket latency by performing a HEAD request with AWS Signature V4 authentication (no SDK required).
new S3Probe( bucket: 'my-bucket', region: 'us-east-1', key: 'AKIAIOSFODNN7EXAMPLE', secret: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', endpoint: null, // Custom endpoint (e.g., MinIO) timeout: 3.0, )
Custom Probes
Implement ProbeInterface to create your own:
use MathiasGrimm\Netwatch\Contract\ProbeInterface; use MathiasGrimm\Netwatch\Result\ProbeResult; class MyProbe implements ProbeInterface { public function probe(): ProbeResult { $start = hrtime(true); // ... your logic ... $elapsed = (hrtime(true) - $start) / 1e6; return new ProbeResult( connectMs: $elapsed, requestMs: 0, totalMs: $elapsed, success: true, ); } public function name(): string { return 'my-probe'; } }
CLI Reference
netwatch:run
Run probes and display latency statistics.
vendor/bin/netwatch netwatch:run [options]
| Option | Short | Description |
|---|---|---|
--config |
-c |
Config file path (default: netwatch.php) |
--iterations |
-i |
Override iteration count for all probes |
--probe |
-p |
Run only a specific probe by name |
--sequential |
Run probes sequentially (default: parallel) | |
--json |
Output results as JSON | |
--without-results |
Exclude individual iteration results from JSON |
netwatch:init
Generate a starter config file.
vendor/bin/netwatch netwatch:init [options]
| Option | Short | Description |
|---|---|---|
--force |
-f |
Overwrite existing netwatch.php |
Output
Table Output (default)
+--------+--------+------------+----------+---------+----------+----------+----------+----------+----------+----------+
| Probe | Status | Iterations | Failures | Metric | Min (ms) | Max (ms) | Avg (ms) | P50 (ms) | P95 (ms) | P99 (ms) |
+--------+--------+------------+----------+---------+----------+----------+----------+----------+----------+----------+
| redis | ok | 10 | 0 | connect | 0.312 | 0.891 | 0.523 | 0.487 | 0.856 | 0.884 |
| | | | | request | 0.098 | 0.234 | 0.142 | 0.131 | 0.221 | 0.231 |
| | | | | total | 0.421 | 1.102 | 0.665 | 0.618 | 1.054 | 1.092 |
+--------+--------+------------+----------+---------+----------+----------+----------+----------+----------+----------+
The Status column and the total P95 cell are colored against the probe's latency thresholds: green/plain when within them, amber at warn, red at crit or when iterations fail.
JSON Output
{
"redis": {
"name": "redis:tcp://127.0.0.1:6379",
"iterations": 10,
"stats": {
"connect_ms": { "min": 0.312, "max": 0.891, "avg": 0.523, "p50": 0.487, "p95": 0.856, "p99": 0.884 },
"request_ms": { "min": 0.098, "max": 0.234, "avg": 0.142, "p50": 0.131, "p95": 0.221, "p99": 0.231 },
"total_ms": { "min": 0.421, "max": 1.102, "avg": 0.665, "p50": 0.618, "p95": 1.054, "p99": 1.092 }
},
"failures": 0,
"status": "ok",
"thresholds": { "warn": 5, "crit": 25 },
"over_warn": 0,
"over_crit": 0,
"results": [
{ "connect_ms": 0.312, "request_ms": 0.098, "total_ms": 0.421, "success": true, "error": null, "status": "ok" }
]
}
}
status is ok / warn / crit (total p95 vs the thresholds) or failing (any iteration failed). Every entry in results also carries its own per-iteration status, and over_warn/over_crit count the samples at or over each threshold.
Statistical Analysis
For each probe, Netwatch runs the configured number of iterations and computes statistics over successful runs. Three timing metrics are tracked:
| Metric | Description |
|---|---|
connect_ms |
Time to establish the connection |
request_ms |
Time to complete the request after connection |
total_ms |
End-to-end latency (connect + request) |
For each metric, the following statistics are computed:
| Stat | Description |
|---|---|
min |
Minimum observed value |
max |
Maximum observed value |
avg |
Arithmetic mean |
p50 |
50th percentile (median) |
p95 |
95th percentile |
p99 |
99th percentile |
Percentiles use linear interpolation between nearest-rank values.
Testing
composer install vendor/bin/pest
Check code style:
vendor/bin/pint --test
License
The MIT License (MIT). Please see License File for more information.









