PHPackages                             systemverk/laravel-api-telemetry - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. systemverk/laravel-api-telemetry

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

systemverk/laravel-api-telemetry
================================

A Laravel package for API request telemetry, providing Redis-backed collection, aggregation and persistence of API request metrics and per-actor usage statistics.

v1.0.0(today)01↑2900%MITPHPPHP ^8.2CI failing

Since Aug 1Pushed todayCompare

[ Source](https://github.com/systemverk/laravel-api-telemetry)[ Packagist](https://packagist.org/packages/systemverk/laravel-api-telemetry)[ RSS](/packages/systemverk-laravel-api-telemetry/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (11)Versions (2)Used By (0)

Laravel API Telemetry
=====================

[](#laravel-api-telemetry)

[![CI](https://github.com/systemverk/laravel-api-telemetry/actions/workflows/ci.yml/badge.svg)](https://github.com/systemverk/laravel-api-telemetry/actions/workflows/ci.yml)[![Latest Version](https://camo.githubusercontent.com/0fc571797ebab1cba60acaec43be4d47c89e706e6464864b572f6f489c010d93/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73797374656d7665726b2f6c61726176656c2d6170692d74656c656d657472792e737667)](https://packagist.org/packages/systemverk/laravel-api-telemetry)[![License](https://camo.githubusercontent.com/eac695831d8fce4d501ba0e2786460620ea8056f67d4e7f654edc127c7b9cd44/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f73797374656d7665726b2f6c61726176656c2d6170692d74656c656d657472792e737667)](LICENSE)

Lightweight API observability for Laravel.

Every API request is buffered in Redis after the response has been sent, flushed to SQL in batches by a scheduled command, and rolled up into daily and monthly per-actor usage statistics.

Requirements
------------

[](#requirements)

SupportedPHP8.2, 8.3, 8.4Laravel11.x, 12.xRedis client`ext-redis` (recommended) or `predis/predis`DatabaseMySQL, MariaDB, PostgreSQL, SQLite, SQL ServerQuick Start
-----------

[](#quick-start)

### 1. Install

[](#1-install)

```
composer require systemverk/laravel-api-telemetry
```

### 2. Run migrations

[](#2-run-migrations)

```
php artisan migrate
```

The package ships its migrations and loads them automatically — there is nothing to publish. Two tables are created:

- `api_request_logs` — raw request telemetry
- `api_usage_stats` — daily and monthly aggregates

### 3. Make sure Redis is configured

[](#3-make-sure-redis-is-configured)

Requests are appended to a Redis list before being flushed to SQL.

- A connection must exist under `database.redis` (or `database.redis.clusters`)
- The package uses the `default` connection unless told otherwise
- Override with `API_TELEMETRY_REDIS_CONNECTION`

If the configured connection does not exist, the middleware records nothing rather than throwing — telemetry never takes an application down.

### 4. Run the scheduler

[](#4-run-the-scheduler)

The commands are registered on the scheduler automatically, but they only run if your scheduler runs.

```
* * * * * php /path/to/app/artisan schedule:run >> /dev/null 2>&1
```

During development:

```
php artisan schedule:work
```

### 5. Verify telemetry is flowing

[](#5-verify-telemetry-is-flowing)

```
php artisan api-telemetry:flush
php artisan api-telemetry:consolidate-daily --date=2026-06-16
php artisan schedule:list
```

What Runs Automatically
-----------------------

[](#what-runs-automatically)

With the default configuration:

1. `LogApiRequest` is appended to the `api` middleware group. If that group does not exist, registration is skipped silently.
2. Four scheduled commands are registered, each `withoutOverlapping()`:

CommandFrequencyPurpose`api-telemetry:flush --max-minutes=5`every minuteRedis buffer → `api_request_logs``api-telemetry:consolidate-daily`daily at 02:00Raw logs → daily stats`api-telemetry:consolidate-monthly`monthly on day 1 at 03:00Daily stats → monthly stats`api-telemetry:prune`daily at 03:10Delete raw logs past retentionHow It Works
------------

[](#how-it-works)

```
request ──► LogApiRequest::handle()      records a start timestamp only
        ──► ...application...
        ──► response sent to the client
        ──► LogApiRequest::terminate()   RPUSH onto api_telemetry:requests:
                                          │
   every minute: api-telemetry:flush ─────┘
        RENAMENX the minute list to a private processing key, insert in batches,
        then delete. The key is tracked in a Redis set until the write is
        confirmed, so a crashed or failed flush is retried on the next run
        instead of silently losing entries.

```

Buffering happens in `terminate()`, so the two Redis round trips are off the critical path of the response.

Configuration
-------------

[](#configuration)

Defaults are usable as-is. Publish the config only if you need to change them:

```
php artisan vendor:publish --tag=api-telemetry-config
```

KeyEnvDefaultDescription`enabled``API_TELEMETRY_ENABLED``true`Master on/off switch`redis.connection``API_TELEMETRY_REDIS_CONNECTION``default`Redis connection name`redis.key_prefix``API_TELEMETRY_REDIS_KEY_PREFIX``api_telemetry:`Redis key prefix`redis.ttl_seconds``API_TELEMETRY_REDIS_TTL_SECONDS``7200`Buffer key TTL (min 60)`database.connection``API_TELEMETRY_DB_CONNECTION``null`Dedicated connection, or the app default`database.tables.request_logs``API_TELEMETRY_TABLE_REQUEST_LOGS``api_request_logs`Raw log table name`database.tables.usage_stats``API_TELEMETRY_TABLE_USAGE_STATS``api_usage_stats`Aggregate table name`database.user_id_type``API_TELEMETRY_USER_ID_TYPE``integer`Set to `string` for UUID/ULID user keys`except`—`['up', 'health']`Paths never recorded (supports `*`)`sampling_rate``API_TELEMETRY_SAMPLING_RATE``1.0`Fraction of requests recorded`privacy.hash_ips``API_TELEMETRY_HASH_IPS``true`Store a salted hash, or nothing at all`privacy.ip_hash_salt``API_TELEMETRY_IP_HASH_SALT``null`Defaults to `app.key``privacy.record_user_agent``API_TELEMETRY_RECORD_USER_AGENT``true`Store the user agent string`request_id_headers`—`X-Request-Id`, `X-Correlation-Id`Correlation-id headers, in priority order`flush_batch_size``API_TELEMETRY_FLUSH_BATCH_SIZE``1000`Rows per insert statement`consolidation_chunk_size``API_TELEMETRY_CONSOLIDATION_CHUNK_SIZE``2000`Read chunk size during rollup`retention_days``API_TELEMETRY_RETENTION_DAYS``90`Raw-log retention period`auto_register_middleware``API_TELEMETRY_AUTO_MIDDLEWARE``true`Auto-append the middleware`middleware_group``API_TELEMETRY_MIDDLEWARE_GROUP``api`Group to append the middleware to`schedule.enabled``API_TELEMETRY_SCHEDULE_ENABLED``true`Auto-register scheduled commands`schedule.flush_minutes``API_TELEMETRY_SCHEDULE_FLUSH_MINUTES``5``--max-minutes` used by flush`schedule.daily_at``API_TELEMETRY_SCHEDULE_DAILY_AT``02:00`Daily consolidation time`schedule.monthly_at``API_TELEMETRY_SCHEDULE_MONTHLY_AT``03:00`Monthly consolidation time`schedule.prune_at``API_TELEMETRY_SCHEDULE_PRUNE_AT``03:10`Prune time### Excluding noisy endpoints

[](#excluding-noisy-endpoints)

```
'except' => [
    'up',
    'health',
    'webhooks/*',
],
```

### Sampling high-volume traffic

[](#sampling-high-volume-traffic)

```
'sampling_rate' => 0.1, // record roughly one request in ten
```

Aggregated counts are **not** scaled back up, so a sampled deployment reports sampled numbers. Keep the rate at `1.0` if the counts must be exact.

### UUID or ULID user keys

[](#uuid-or-ulid-user-keys)

Set `database.user_id_type` to `string` **before** running the migrations. With the default `integer` setting, non-numeric identifiers are recorded as `null`rather than being silently coerced to `0`.

Manual Wiring
-------------

[](#manual-wiring)

For full control, disable both switches:

```
API_TELEMETRY_AUTO_MIDDLEWARE=false
API_TELEMETRY_SCHEDULE_ENABLED=false
```

Then register everything yourself:

```
// bootstrap/app.php
use Systemverk\LaravelApiTelemetry\Http\Middleware\LogApiRequest;

->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [LogApiRequest::class]);
})

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('api-telemetry:flush --max-minutes=5')->everyMinute()->withoutOverlapping();
Schedule::command('api-telemetry:consolidate-daily')->dailyAt('02:00');
Schedule::command('api-telemetry:consolidate-monthly')->monthlyOn(1, '03:00');
Schedule::command('api-telemetry:prune')->dailyAt('03:10');
```

Data Model
----------

[](#data-model)

All timestamps are stored in **UTC**, independent of `app.timezone`.

### `api_request_logs` (raw)

[](#api_request_logs-raw)

One row per recorded request:

ColumnNotes`requested_at`UTC`method`, `path`, `route_name``path` truncated to 1024 chars`status_code`, `duration_ms``user_id`Nullable; integer or string per config`ip_hash`Salted SHA-256, or null`user_agent`Truncated to 512 chars, or null`request_id`First matching correlation header, 64 chars### `api_usage_stats` (aggregated)

[](#api_usage_stats-aggregated)

One row per `(period_type, period_start, actor_key)`:

- `period_type` is `day` or `month`
- `actor_key` is `guest` or `user:{id}`
- `total_requests` plus a counter per status class: `responses_1xx` through `responses_5xx`

Consolidation is idempotent — reruns upsert rather than accumulate, so a backfill can be re-run safely:

```
php artisan api-telemetry:consolidate-daily --date=2026-06-16
php artisan api-telemetry:consolidate-monthly --month=2026-06
```

Invalid `--date` or `--month` values produce a clear error and exit code 1.

Querying the Data
-----------------

[](#querying-the-data)

Both models are plain Eloquent models and are part of the public API.

```
use Systemverk\LaravelApiTelemetry\Models\ApiRequestLog;
use Systemverk\LaravelApiTelemetry\Models\ApiUsageStat;

// Slowest endpoints in the last 24 hours
ApiRequestLog::query()
    ->where('requested_at', '>=', now()->utc()->subDay())
    ->selectRaw('path, count(*) as hits, avg(duration_ms) as avg_ms')
    ->groupBy('path')
    ->orderByDesc('avg_ms')
    ->limit(10)
    ->get();

// Server errors today
ApiRequestLog::query()->statusClass(5)->whereDate('requested_at', today())->count();

// Monthly usage for one customer
ApiUsageStat::query()->monthly()->where('actor_key', 'user:42')->get();
```

Privacy
-------

[](#privacy)

- IP addresses are never stored in clear text — only a salted SHA-256 digest, or nothing when `privacy.hash_ips` is disabled
- Request and response bodies are never recorded
- No headers are recorded except the configured correlation id
- Request **paths are stored verbatim**. If your API puts secrets in the URL path, exclude those routes via `except`

Rotating `APP_KEY` changes the default IP hash salt, so hashes recorded before and after a rotation will not correlate. Set an explicit `API_TELEMETRY_IP_HASH_SALT` if that matters to you.

Testing
-------

[](#testing)

```
composer install
composer test
composer analyse
```

The suite runs against SQLite in memory with an in-memory Redis double, so no services are required.

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md). Security issues: [SECURITY.md](SECURITY.md).

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 70% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0f1c3128c4a2e36805c4cee9d6e980ba009cc581baaf6611424a29b15a9a3e4a?d=identicon)[ctorno](/maintainers/ctorno)

---

Top Contributors

[![ctorno](https://avatars.githubusercontent.com/u/60401369?v=4)](https://github.com/ctorno "ctorno (7 commits)")[![semantic-release-bot](https://avatars.githubusercontent.com/u/32174276?v=4)](https://github.com/semantic-release-bot "semantic-release-bot (3 commits)")

---

Tags

apilaravelloggingmonitoringredisMetricsanalyticstelemetry

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/systemverk-laravel-api-telemetry/health.svg)

```
[![Health](https://phpackages.com/badges/systemverk-laravel-api-telemetry/health.svg)](https://phpackages.com/packages/systemverk-laravel-api-telemetry)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M352](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M275](/packages/laravel-ai)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M144](/packages/laravel-pulse)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

78727.1M205](/packages/laravel-mcp)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M180](/packages/spatie-laravel-health)[api-platform/laravel

API Platform support for Laravel

58174.6k18](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
