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
- 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-03-13 12:00:31 UTC
README
Network service latency probing tool for PHP. Measures connectivity and response times to Redis, PostgreSQL, MySQL, S3, HTTP endpoints, and raw TCP services with statistical analysis.
Alpha version — This package is under active development. The API may change at any time, including breaking changes, without prior notice. Use at your own risk.
TODO
- Include hostname/identification on the output
- Running
php artisan netwatch:runcreates file calledhost=;port=;dbname=
Maybe
- Post results to some endpoint after
php artisan netwatch:run - Detect env/cached config. Maybe boot application once and cache it with a md5(.env)
- Maybe
Concurrency::driver(’queue’)for async when calling/netwatch/health - Store & Cleanup of metrics to view on a dashboard
- Use Inertia (3) for the dashboard with polling
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
- 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
Requirements
- PHP 8.3+
ext-curl(for S3 probe)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'), ], ], ], 'redis' => [ 'enabled' => env('NETWATCH_PROBE_REDIS_ENABLED', false), 'probe' => [ PhpRedisProbe::class => [ env('REDIS_HOST').':'.env('REDIS_PORT'), env('REDIS_USERNAME'), env('REDIS_PASSWORD'), ], ], ], '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 |
Health Dashboard
Enable the health dashboard route by setting NETWATCH_HEALTH_ENABLED=true in your .env:
NETWATCH_HEALTH_ENABLED=true NETWATCH_PATH=netwatch
Access the dashboard at /netwatch/health.
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. |
Parameters can be combined: /netwatch/health?probes=redis,database&format=json&without_results=1
HTML view — interactive dashboard with per-probe latency stats:
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 local environments. To configure access in other environments, 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_REDIS_ENABLED=false # Enable Redis probe 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
Standalone (without Laravel)
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
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 | Iterations | Failures | Metric | Min (ms) | Max (ms) | Avg (ms) | P50 (ms) | P95 (ms) | P99 (ms) |
+--------+------------+----------+---------+----------+----------+----------+----------+----------+----------+
| redis | 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 |
+--------+------------+----------+---------+----------+----------+----------+----------+----------+----------+
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,
"results": [ ... ]
}
}
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
License
MIT




