mathiasgrimm/netwatch

Network service latency probing tool for PHP — measures Redis, PostgreSQL, MySQL, S3, HTTP endpoints with statistical analysis

Maintainers

Package info

github.com/mathiasgrimm/netwatch

pkg:composer/mathiasgrimm/netwatch

Statistics

Installs: 66

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-03-13 11:57 UTC

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:run creates file called host=;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

CLI Table 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:

Health Dashboard

JSON panel — view raw JSON data directly within the dashboard:

Health Dashboard - JSON Panel

JSON API response — append ?format=json for a raw JSON endpoint:

Health Dashboard - JSON API Response

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 ProbeInterface object, 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 with php 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": [ ... ]
  }
}

CLI JSON Output

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