tsitsishvili/elastic-audit 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

tsitsishvili/elastic-audit

Composer 安装命令:

composer require tsitsishvili/elastic-audit

包简介

Laravel package that logs third-party HTTP traffic (outgoing requests and incoming callbacks) and actor/model activity to a dedicated Elasticsearch cluster, with redaction, queued indexing, sampling, and optional dashboards.

README 文档

README

Laravel package that logs third-party HTTP traffic and actor/model activity to a dedicated Elasticsearch cluster.

The package is intended for internal applications that need a consistent audit/debug trail for provider calls, callbacks, latency, status codes, entity context, and sanitized request/response payload previews.

It has two independent subsystems that share a single Elasticsearch connection:

  • HTTP logs — the HttpLog facade logs outgoing third-party requests and incoming callbacks (config http_logs.php, commands http-logs:*).
  • Activity logs — the ActivityLog facade (and ActivityLoggable trait) logs actor actions and Eloquent model changes (config activity_logs.php, commands activity-logs:*).

Each subsystem has its own config, Elasticsearch index/aliases, queue, console commands, and optional dashboard, so you can enable only what you need.

Project Documents

Table of Contents

Quick Start

  1. Add the GitLab repository to the consuming application's composer.json.

  2. Install the package:

    composer require tsitsishvili/elastic-audit:^1.0
  3. Publish the config files and enum stubs:

    php artisan vendor:publish --tag=elastic-audit
  4. Register the application's provider, event type, and entity type enums in config/http_logs.php.

  5. Configure Elasticsearch and enable logging in .env.

  6. Create the Elasticsearch index and aliases:

    php artisan http-logs:create-index
  7. Run a queue worker for the configured logs queue:

    php artisan queue:work --queue=default
  8. Use HttpLog::make(...) for outgoing provider calls or IncomingHttpLogMiddleware for incoming callbacks.

Requirements

  • PHP ^8.3 || ^8.4
  • Laravel ^12.0 || ^13.0
  • Elasticsearch PHP client ^8.0 || ^9.0
  • A queue worker, because logs are indexed through queued jobs

Installation

Add the package repository to the consuming application's composer.json.

{
  "repositories": [
    {
      "type": "vcs",
      "url": "git@gitlab.tsitsishvili.ge:laravel-packages/http-logs.git"
    }
  ]
}

Install a tagged version:

composer require tsitsishvili/elastic-audit:^1.0

Laravel auto-discovers the package service provider.

Publish Configuration

php artisan vendor:publish --tag=elastic-audit

This publishes:

config/http_logs.php
config/log_elasticsearch.php
app/Enums/ElasticAudit/Provider.php
app/Enums/ElasticAudit/EventType.php
app/Enums/ElasticAudit/EntityType.php

The enum stubs are starting-point implementations of the three package contracts. Edit them to match the providers and event types used by the application.

Environment Variables

HTTP_LOGS_ENABLED=true
HTTP_LOGS_QUEUE=default
HTTP_LOGS_SAMPLE_RATE=1.0
HTTP_LOGS_BODY_PREVIEW_BYTES=4096
HTTP_LOGS_BODY_MAX_BYTES=32768
HTTP_LOGS_PAYMENT_BODY_MODE=preview

HTTP_LOGS_DASHBOARD_ENABLED=true
ELASTIC_AUDIT_DASHBOARD_PREFIX=logger
HTTP_LOGS_DASHBOARD_PATH=third-party

LOG_ELASTICSEARCH_HOST=localhost
LOG_ELASTICSEARCH_PORT=9200
LOG_ELASTICSEARCH_SCHEME=http
LOG_ELASTICSEARCH_USERNAME=
LOG_ELASTICSEARCH_PASSWORD=
LOG_ELASTICSEARCH_INDEX_PREFIX=my_app
LOG_ELASTICSEARCH_REPLICAS=1
Variable Description
HTTP_LOGS_ENABLED Set to true to enable logging.
HTTP_LOGS_QUEUE Queue name for log jobs.
HTTP_LOGS_SAMPLE_RATE Float 0.01.0. 1.0 = log all, 0.0 = log none. Intermediate values sample randomly.
HTTP_LOGS_BODY_PREVIEW_BYTES Max bytes stored as sanitized body preview.
HTTP_LOGS_BODY_MAX_BYTES Max raw body size before truncation.
HTTP_LOGS_PAYMENT_BODY_MODE Body handling mode for payment providers (preview or omit).
HTTP_LOGS_DASHBOARD_ENABLED Set to true to register the web dashboard routes.
ELASTIC_AUDIT_DASHBOARD_PREFIX Shared URL prefix for both dashboards (default logger). Composes as {prefix}/{path}. Set to empty string to serve at the root.
HTTP_LOGS_DASHBOARD_PATH This dashboard's subpath under the group prefix (default third-party). Served at /logger/third-party.

The package writes to aliases based on LOG_ELASTICSEARCH_INDEX_PREFIX:

my_app_http_logs
my_app_http_logs_write

Configuration Reference

http_logs.php

Key Default Description
enabled false Enables or disables third-party HTTP logging. When disabled, no log jobs are dispatched.
queue default Queue name used by LogHttpRequestJob.
sample_rate 1.0 Float between 0.0 and 1.0. 1.0 logs every request, 0.0 logs none, intermediate values use probabilistic sampling (e.g. 0.1 logs ~10%). Controlled by HTTP_LOGS_SAMPLE_RATE.
body_preview_bytes 4096 Maximum number of sanitized body bytes stored as preview.
body_max_bytes 32768 Maximum raw body size considered before truncation handling.
payment_body_mode preview Controls payment provider body handling.
index_alias {prefix}_http_logs Elasticsearch read alias.
index_alias_write {prefix}_http_logs_write Elasticsearch write alias.
enums.provider null Backed enum class implementing ProviderContract.
enums.event_type null Backed enum class implementing EventTypeContract.
enums.entity_type null Backed enum class implementing EntityTypeContract.
enums.entity_type_default none Fallback entity type value for incoming callback logs.
payment_provider_values [] Provider enum values that should use payment-specific redaction.
dashboard.enabled true Registers the web dashboard routes. Set to false to hide the UI entirely.
dashboard.prefix logger Shared group URL segment placed before every dashboard. Both dashboards read ELASTIC_AUDIT_DASHBOARD_PREFIX; changing it moves both at once. Set to '' to serve at the root.
dashboard.path third-party This dashboard's own subpath under the group prefix. Composes with prefix as {prefix}/{path}, e.g. /logger/third-party.
dashboard.middleware ['web'] Middleware applied to dashboard routes. The package always appends its authorization middleware after this stack.
dashboard.per_page 25 Number of log rows shown per page in the list view.

log_elasticsearch.php

Key Default Description
hosts.0.host localhost Elasticsearch host used for log indexing.
hosts.0.port 9200 Elasticsearch port.
hosts.0.scheme http Elasticsearch scheme, usually http or https.
basicAuthentication.username empty string Optional Elasticsearch basic auth username.
basicAuthentication.password empty string Optional Elasticsearch basic auth password.
index_prefix app_logs Prefix used when creating physical indexes and aliases.
replicas 1 Number of Elasticsearch replicas for the logs index. Use 0 for single-node staging clusters.

Register Application Enums

Each application defines its own provider, event type, and entity type enums. These enums must implement the package contracts.

<?php

namespace App\Enums\ElasticAudit;

use Tsitsishvili\ElasticAudit\Contracts\ProviderContract;

enum Provider: string implements ProviderContract
{
    case Delivery = 'delivery';
    case Payment = 'payment';

    public function getValue(): string
    {
        return $this->value;
    }
}
<?php

namespace App\Enums\ElasticAudit;

use Tsitsishvili\ElasticAudit\Contracts\EventTypeContract;

enum EventType: string implements EventTypeContract
{
    case DeliveryOrderCreate = 'delivery_order_create';
    case DeliveryStatusCallback = 'delivery_status_callback';
    case PaymentCallback = 'payment_callback';

    public function getValue(): string
    {
        return $this->value;
    }
}
<?php

namespace App\Enums\ElasticAudit;

use Tsitsishvili\ElasticAudit\Contracts\EntityTypeContract;

enum EntityType: string implements EntityTypeContract
{
    case Order = 'order';
    case Payment = 'payment';
    case None = 'none';

    public function getValue(): string
    {
        return $this->value;
    }
}

Register them in config/http_logs.php:

'enums' => [
    'provider' => App\Enums\Provider::class,
    'event_type' => App\Enums\EventType::class,
    'entity_type' => App\Enums\EntityType::class,
    'entity_type_default' => 'none',
],

'payment_provider_values' => [
    App\Enums\Provider::Payment->value,
],

Providers listed in payment_provider_values use the payment redactor.

What Gets Logged

Each document is built from HttpLogData and includes operational metadata, entity context, sanitized payload data, and failure information.

Field Description
event_id Unique ULID for the log document.
@timestamp Time the log data was created.
request_id Correlation ULID shared by the log context.
provider Provider enum value, for example delivery or payment.
event_type Event type enum value, for example delivery_order_create.
direction outgoing for provider calls or incoming for callbacks.
http.method HTTP method, for example GET, POST, or PATCH.
http.url URL without query string. Query strings are stripped to avoid storing tokens or API keys.
http.host Parsed host from the URL.
http.path Parsed path from the URL.
http.status_code Response status code when available.
http.status_class Status class such as 2xx, 4xx, or 5xx.
latency_ms Request or callback handling duration in milliseconds.
entity.type Entity type from the log context, for example order.
entity.id Internal entity identifier from the log context.
external_id Optional external provider identifier.
user_id Optional application user id.
attempt Queue/job attempt or request attempt value.
success Boolean success flag.
retention_days Retention window used by http-logs:prune.
request Sanitized request headers, body preview, body hash, and truncation flag.
response Sanitized response headers, body preview, body hash, and truncation flag.
error.class Exception class for failed outgoing calls when available.
error.message Sanitized exception message for failed outgoing calls when available.

Both incoming callback logs and outgoing request logs store request and response payloads. For incoming callbacks the response is captured automatically by IncomingHttpLogMiddleware, or when you pass the response to HttpLog::logIncoming(...). Outgoing request logs capture the provider's response when available.

Create Elasticsearch Index

Create the physical index and attach read/write aliases:

php artisan http-logs:create-index

This command refuses to create the logs index when the configured logs Elasticsearch host matches the product-search Elasticsearch host.

Logging Outgoing Requests

Use HttpLog::make(...) to obtain a logging-aware HTTP client instead of using Laravel's Http facade directly.

<?php

namespace App\Services;

use App\Enums\EntityType;
use App\Enums\EventType;
use App\Enums\Provider;
use Tsitsishvili\ElasticAudit\DataTransferObjects\HttpLogContext;
use Tsitsishvili\ElasticAudit\Facades\HttpLog;

class DeliveryProviderClient
{
    public function createOrder(int $orderId): array
    {
        $context = HttpLogContext::forEntity(
            entityType: EntityType::Order,
            entityId: (string) $orderId,
            externalId: null,
            userId: auth()->id(),
            retentionDays: 360,
        );

        $response = HttpLog::make(
            provider: Provider::Delivery,
            eventType: EventType::DeliveryOrderCreate,
            context: $context,
        )
            ->timeout(10)
            ->retry(2, 200)
            ->withToken(config('services.delivery.token'))
            ->post('https://provider.example/orders', [
                'order_id' => $orderId,
            ]);

        return $response->json();
    }
}

HttpLog::make(...) returns Laravel's own HTTP client (Illuminate\Http\Client\PendingRequest) with an outgoing-request logging middleware already attached. There is no custom wrapper — the entire Laravel HTTP client API is available and every request you make through it is logged automatically:

HttpLog::make($provider, $eventType, $context)->get($url, $query);
HttpLog::make($provider, $eventType, $context)->post($url, $data);
HttpLog::make($provider, $eventType, $context)
    ->acceptJson()
    ->withBasicAuth($username, $password)
    ->withQueryParameters(['page' => 1])
    ->post($url, $data);

// Form-encoded body (application/x-www-form-urlencoded) — native Laravel, logged the same way:
HttpLog::make($provider, $eventType, $context)
    ->asForm()
    ->post('https://provider.example/oauth/token', ['grant_type' => 'client_credentials']);

Because logging happens at the transport (Guzzle middleware) layer, the wire format is irrelevant to logging: JSON, form, multipart, etc. are all redacted and stored uniformly, and no method call can bypass logging.

The original provider call behavior is preserved. If the provider request fails, the package dispatches the log job and rethrows the original exception.

Logging Incoming Callbacks

Register the middleware on callback routes:

use App\Http\Controllers\DeliveryCallbackController;
use Illuminate\Support\Facades\Route;
use Tsitsishvili\ElasticAudit\Http\Middleware\IncomingHttpLogMiddleware;

Route::post('/callbacks/delivery', DeliveryCallbackController::class)
    ->middleware(IncomingHttpLogMiddleware::class);

Set trusted request attributes server-side before the response is returned. The middleware reads these attributes after the request has been handled.

<?php

namespace App\Http\Controllers;

use App\Enums\EntityType;
use App\Enums\EventType;
use App\Enums\Provider;
use Illuminate\Http\Request;

class DeliveryCallbackController
{
    public function __invoke(Request $request)
    {
        $orderId = (string) $request->input('order_id', 'unknown');

        $request->attributes->set('third_party_provider', Provider::Delivery->value);
        $request->attributes->set('third_party_event_type', EventType::DeliveryStatusCallback->value);
        $request->attributes->set('third_party_entity_type', EntityType::Order->value);
        $request->attributes->set('third_party_entity_id', $orderId);

        // Handle the callback...

        return response()->json(['received' => true]);
    }
}

Do not resolve provider or event type from URL segments or request input. Set these values from application code so user-controlled data cannot spoof log metadata.

The middleware automatically logs the response it returns (status code, headers, and sanitized body) alongside the request — no extra code is required.

Manual Incoming Logging

If middleware is not a good fit, call HttpLog::logIncoming(...) directly.

use App\Enums\EntityType;
use App\Enums\EventType;
use App\Enums\Provider;
use Illuminate\Http\Request;
use Tsitsishvili\ElasticAudit\DataTransferObjects\HttpLogContext;
use Tsitsishvili\ElasticAudit\Facades\HttpLog;

public function webhook(Request $request)
{
    $context = HttpLogContext::forEntity(
        entityType: EntityType::Payment,
        entityId: (string) $request->input('payment_id', 'unknown'),
        retentionDays: 180,
    );

    // Build the response first so it can be logged, then return the same instance.
    $response = response()->json(['ok' => true]);

    HttpLog::logIncoming(
        request: $request,
        provider: Provider::Payment,
        eventType: EventType::PaymentCallback,
        context: $context,
        latencyMs: 0,
        httpStatusCode: $response->getStatusCode(),
        success: true,
        response: $response, // optional — captures sanitized response headers and body
    );

    return $response;
}

Queues

Logs are dispatched through LogHttpRequestJob.

Run a worker for the configured queue:

php artisan queue:work --queue=default

If you use a dedicated queue:

HTTP_LOGS_QUEUE=logs
php artisan queue:work --queue=logs

Dashboard

The package ships a Horizon-style web dashboard for browsing logged requests. It reads directly from the Elasticsearch read alias and is rendered with server-side Blade (Tailwind + Alpine via CDN) — there is no build step and no assets to compile or publish.

Once the package is installed it is served (by default) at:

/logger/third-party

It provides three views:

  • Overview — totals, success rate, 4xx/5xx counts, average/p95 latency, a throughput chart, and breakdowns by status class and provider.
  • Logs — a paginated, filterable table (provider, event type, direction, status class, success, entity id, and a date range). Each row links to its detail view.
  • Log detail — full operational metadata plus sanitized request/response headers, body previews, body hashes, and error information for a single log document.

Access control

Access is gated by an authorization callback. By default the dashboard is only reachable in the local environment — every other environment is denied until you grant access explicitly.

Register a callback from any service provider's boot() method (for example App\Providers\AppServiceProvider):

use Tsitsishvili\ElasticAudit\Dashboard\Dashboard;

public function boot(): void
{
    Dashboard::auth(fn ($request) => $request->user()?->isAdmin() === true);
}

The callback receives the current Illuminate\Http\Request and must return a boolean. Requests that fail it receive a 403.

Configuration

// config/http_logs.php
'dashboard' => [
    'enabled'    => env('HTTP_LOGS_DASHBOARD_ENABLED', true),
    'prefix'     => env('ELASTIC_AUDIT_DASHBOARD_PREFIX', 'logger'),
    'path'       => env('HTTP_LOGS_DASHBOARD_PATH', 'third-party'),
    'middleware' => ['web'],
    'per_page'   => 25,
],

Set enabled to false to omit the routes completely. The package always appends its own authorization middleware after the configured middleware stack.

Customizing the views

To override the bundled Blade templates, publish them and edit the copies in your application:

php artisan vendor:publish --tag=elastic-audit-views

This publishes the views to resources/views/vendor/elastic-audit.

Pruning Old Logs

Each log document stores retention_days from HttpLogContext.

Run pruning manually:

php artisan http-logs:prune

Schedule it in the consuming application:

use Illuminate\Support\Facades\Schedule;

Schedule::command('http-logs:prune')->dailyAt('03:00');

Redaction Notes

The package sanitizes headers, request bodies, response bodies, and exception messages before indexing. Query strings are stripped from stored URLs because they can contain API keys or tokens.

Body storage is controlled by:

HTTP_LOGS_BODY_PREVIEW_BYTES=4096
HTTP_LOGS_BODY_MAX_BYTES=32768
HTTP_LOGS_PAYMENT_BODY_MODE=preview

For payment providers, add the provider enum value to payment_provider_values.

Sampling

Control what fraction of requests are logged using the sample_rate config key (or HTTP_LOGS_SAMPLE_RATE env variable).

Value Behaviour
1.0 Every request is logged (default).
0.0 No requests are logged.
0.1 ~10% of requests are logged, chosen at random.

Sampling is applied independently to each request via mt_rand() before any payload is built or any job dispatched, so skipped requests have zero overhead beyond the random check. Setting sample_rate to 1.0 skips the random check entirely.

# Log roughly 25% of requests
HTTP_LOGS_SAMPLE_RATE=0.25

Note: Sampling is statistical. At low rates and low traffic volumes the actual percentage may deviate noticeably from the configured value.

Troubleshooting

No logs are created

Check that logging is enabled:

HTTP_LOGS_ENABLED=true

Also confirm the consuming application is using HttpLog::make(...) for outgoing requests or IncomingHttpLogMiddleware / HttpLog::logIncoming(...) for incoming callbacks.

Log jobs are dispatched but documents do not appear in Elasticsearch

Check that a queue worker is running for the configured queue:

php artisan queue:work --queue=default

If HTTP_LOGS_QUEUE=logs, run:

php artisan queue:work --queue=logs

Elasticsearch index or alias is missing

Create the index and aliases:

php artisan http-logs:create-index

Confirm LOG_ELASTICSEARCH_INDEX_PREFIX matches the alias you are querying.

Cannot connect to Elasticsearch

Verify the logs cluster settings:

LOG_ELASTICSEARCH_HOST=localhost
LOG_ELASTICSEARCH_PORT=9200
LOG_ELASTICSEARCH_SCHEME=http
LOG_ELASTICSEARCH_USERNAME=
LOG_ELASTICSEARCH_PASSWORD=

The http-logs:create-index command will fail if the logs Elasticsearch host matches the configured product-search Elasticsearch host.

Incoming callback logs are skipped

The callback middleware only logs when these request attributes are set by server-side code:

$request->attributes->set('third_party_provider', Provider::Delivery->value);
$request->attributes->set('third_party_event_type', EventType::DeliveryStatusCallback->value);
$request->attributes->set('third_party_entity_type', EntityType::Order->value);
$request->attributes->set('third_party_entity_id', $orderId);

Provider, event type, and entity type enum classes must also be registered in config/http_logs.php.

Payment data appears too detailed

Add payment provider enum values to payment_provider_values:

'payment_provider_values' => [
    App\Enums\Provider::Payment->value,
],

Then review:

HTTP_LOGS_PAYMENT_BODY_MODE=preview
HTTP_LOGS_BODY_PREVIEW_BYTES=4096
HTTP_LOGS_BODY_MAX_BYTES=32768

Config changes are not applied

Clear Laravel's cached config:

php artisan config:clear

Development / Testing

Install package dependencies:

composer install

Validate Composer metadata:

composer validate --no-check-publish

The package includes phpunit.xml with separate Unit and Feature test suites.

Before running the tests in a fresh checkout, make sure the package has these development dependencies installed:

composer require --dev phpunit/phpunit orchestra/testbench

Run all tests:

vendor/bin/phpunit

Run only unit tests:

vendor/bin/phpunit --testsuite Unit

Run only feature tests:

vendor/bin/phpunit --testsuite Feature

Useful checks before opening a merge request:

composer validate --no-check-publish
vendor/bin/phpunit

Testing Example

Integration-style tests

Fake Laravel's bus and HTTP client so the facade still executes real application code but no real HTTP calls are made and no log jobs are dispatched to a queue:

use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Http;
use Tsitsishvili\ElasticAudit\Jobs\LogHttpRequestJob;

Bus::fake();

Http::fake([
    'https://provider.example/*' => Http::response(['ok' => true], 200),
]);

// Execute code that calls HttpLog::make(...)

Bus::assertDispatched(LogHttpRequestJob::class);

Unit tests — faking the facade with a spy

Use HttpLog::spy() to replace the underlying manager with a Mockery spy. The spy records every call but executes no real code, so no HTTP requests are made and no jobs are dispatched. This is appropriate when the subject under test calls the facade and you want to assert what it called without wiring up the full stack.

use App\Enums\ElasticAudit\EventType;
use App\Enums\ElasticAudit\Provider;
use Tsitsishvili\ElasticAudit\DataTransferObjects\HttpLogContext;
use Tsitsishvili\ElasticAudit\Facades\HttpLog;

HttpLog::spy();

// Execute code that calls HttpLog::make(...)

HttpLog::shouldReceive('make')
    ->once()
    ->with(
        Provider::Delivery,
        EventType::DeliveryOrderCreate,
        \Mockery::type(HttpLogContext::class),
    );

To assert the facade was never called:

HttpLog::spy();

// Execute code that should NOT trigger logging

HttpLog::shouldReceive('make')->never();

Unit tests — controlling responses with Http::fake()

Because make() returns Laravel's real PendingRequest, you stub provider responses with Http::fake() and assert the outgoing call with Http::assertSent() — exactly as you would test any code that uses the Http facade. There is no custom client to mock.

use Illuminate\Support\Facades\Http;

Http::fake([
    'provider.example/*' => Http::response(['ok' => true], 200),
]);

// Execute code under test — it calls HttpLog::make(...)->post('https://provider.example/orders', ...)

Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
    return $request->url() === 'https://provider.example/orders'
        && $request['order_id'] === 1;
});

To assert the request was sent as a form, use $request->isForm(); for JSON, $request->isJson().

If you also want to assert that the logging job was queued, fake the bus and check for the job:

use Illuminate\Support\Facades\Bus;
use Tsitsishvili\ElasticAudit\Jobs\LogHttpRequestJob;

Bus::fake();
Http::fake(['provider.example/*' => Http::response(['ok' => true], 200)]);

// Execute code under test

Bus::assertDispatched(LogHttpRequestJob::class);

If you only need to assert that make() was called with a particular provider/event/context (and do not care about the HTTP exchange), you can still spy on the facade as shown above with HttpLog::spy() + shouldReceive('make').

Searching Logs in Elasticsearch

The package writes documents to the read alias configured as index_alias in config/http_logs.php (defaults to {prefix}_http_logs).

Using the PHP client

Resolve LogElasticsearchClientInterface from the container and call search():

use Tsitsishvili\ElasticAudit\Services\Elasticsearch\LogElasticsearchClientInterface;

$client = app(LogElasticsearchClientInterface::class);

$results = $client->search([
    'index' => config('http_logs.index_alias'),
    'body'  => [
        'query' => [
            'bool' => [
                'must'   => [
                    ['term' => ['provider'     => 'delivery']],
                    ['term' => ['entity.type'  => 'order']],
                    ['term' => ['entity.id'    => (string) $orderId]],
                ],
                'filter' => [
                    ['range' => ['@timestamp' => ['gte' => 'now-7d', 'lt' => 'now']]],
                ],
            ],
        ],
        'sort' => [['@timestamp' => ['order' => 'desc']]],
        'size' => 50,
    ],
]);

$hits = $results['hits']['hits'];

Common filter combinations

All failed outgoing requests for a provider:

{
  "query": {
    "bool": {
      "must": [
        { "term": { "provider":   "delivery" } },
        { "term": { "direction":  "outgoing" } },
        { "term": { "success":    false      } }
      ]
    }
  },
  "sort": [{ "@timestamp": { "order": "desc" } }],
  "size": 100
}

All logs for a specific entity (e.g., order 42) across providers:

{
  "query": {
    "bool": {
      "must": [
        { "term": { "entity.type": "order" } },
        { "term": { "entity.id":   "42"    } }
      ]
    }
  },
  "sort": [{ "@timestamp": { "order": "desc" } }],
  "size": 50
}

Slow outgoing requests (latency over 3 seconds) in the last 24 hours:

{
  "query": {
    "bool": {
      "must":   [{ "term":  { "direction": "outgoing" } }],
      "filter": [
        { "range": { "http.latency_ms": { "gte": 3000 } } },
        { "range": { "@timestamp":      { "gte": "now-24h" } } }
      ]
    }
  },
  "sort": [{ "http.latency_ms": { "order": "desc" } }],
  "size": 50
}

5xx responses by provider in the last hour:

{
  "query": {
    "bool": {
      "must":   [{ "term":  { "http.status_class": "5xx" } }],
      "filter": [{ "range": { "@timestamp": { "gte": "now-1h" } } }]
    }
  },
  "aggs": {
    "by_provider": {
      "terms": { "field": "provider", "size": 20 }
    }
  },
  "size": 0
}

Incoming callbacks for a specific event type:

{
  "query": {
    "bool": {
      "must": [
        { "term": { "direction":  "incoming"                } },
        { "term": { "event_type": "delivery_status_callback" } }
      ],
      "filter": [
        { "range": { "@timestamp": { "gte": "now-7d" } } }
      ]
    }
  },
  "sort": [{ "@timestamp": { "order": "desc" } }],
  "size": 50
}

All queries can be run directly in Kibana Dev Tools against the read alias. Replace my_app_http_logs with the alias configured for your application.

Activity Logging

An independent subsystem for recording what actors did or changed — user actions and Eloquent model changes — indexed to a dedicated Elasticsearch index. It is fully decoupled from the HTTP logger: the two share only the Elasticsearch client and the service provider. Each has its own config, index/aliases, DTOs, job, indexer, commands, and dashboard.

How It Works

The capture → queue → index pipeline mirrors the HTTP logger:

ActivityLogger::record()  →  ActivityLogData (immutable DTO)  →  LogActivityJob (queued)
    →  ActivityLogIndexer  →  LogElasticsearchClientInterface  →  activity write alias

Capture never throws and is gated by activity_logs.enabled. Indexing happens asynchronously on the configured queue. The document ID is sha256(eventId).

Activity Configuration

Published alongside the other configs under the elastic-audit tag:

php artisan vendor:publish --tag=elastic-audit

config/activity_logs.php:

return [
    'enabled'           => env('ACTIVITY_LOGS_ENABLED', true),
    'queue'             => env('ACTIVITY_LOGS_QUEUE', 'default'),
    'retention_days'    => 360,

    'index_alias'       => strtolower(env('LOG_ELASTICSEARCH_INDEX_PREFIX', env('APP_NAME'))) . '_activity_logs',
    'index_alias_write' => strtolower(env('LOG_ELASTICSEARCH_INDEX_PREFIX', env('APP_NAME'))) . '_activity_logs_write',

    'dashboard' => [
        'enabled'    => env('ACTIVITY_LOGS_DASHBOARD_ENABLED', true),
        'prefix'     => env('ELASTIC_AUDIT_DASHBOARD_PREFIX', 'logger'),
        'path'       => env('ACTIVITY_LOGS_DASHBOARD_PATH', 'activity'),
        'middleware' => ['web'],
        'per_page'   => 25,
    ],
];

It reuses the existing log_elasticsearch.php connection — activity logs are never written to the product-search cluster.

Relevant environment variables:

Variable Default Purpose
ACTIVITY_LOGS_ENABLED true Master on/off switch for capture
ACTIVITY_LOGS_QUEUE default Queue the indexing job is dispatched to
ACTIVITY_LOGS_DASHBOARD_ENABLED true Register the dashboard routes
ELASTIC_AUDIT_DASHBOARD_PREFIX logger Shared URL prefix for both dashboards. Composes as {prefix}/{path}. Set to '' for root paths.
ACTIVITY_LOGS_DASHBOARD_PATH activity This dashboard's subpath under the group prefix. Served at /logger/activity.

Create the Activity Index

php artisan activity-logs:create-index

Creates the physical index (<prefix>_activity_logs_<timestamp>) with a dynamic: strict mapping and attaches the read/write aliases.

Manual Logging

Use the ActivityLog facade. Build an ActivityLogContext describing the actor and entity, then record an action with an optional field-level diff and metadata.

use Tsitsishvili\ElasticAudit\Facades\ActivityLog;
use Tsitsishvili\ElasticAudit\DataTransferObjects\ActivityLogContext;
use App\Enums\ElasticAudit\EntityType;

// User-driven change with a before/after diff
ActivityLog::record(
    action: 'order.status_updated',
    context: ActivityLogContext::forActor(
        actorType: 'user',
        actorId: $userId,
        entityType: EntityType::Order,
        entityId: (string) $order->id,
        requestId: $request->header('X-Request-ID'), // optional; auto-ULID if omitted
    ),
    changes: [
        'status' => ['old' => 'pending', 'new' => 'paid'],
        'amount' => ['old' => 100,       'new' => 95],
    ],
);

// System/cron action — no actor id, marked as failed
ActivityLog::record(
    action: 'invoice.auto_cancelled',
    context: ActivityLogContext::forActor(
        actorType: 'cron',
        actorId: null,
        entityType: EntityType::Invoice,
        entityId: (string) $invoice->id,
    ),
    metadata: ['reason' => 'payment_timeout'],
    success: false,
    errorClass: TimeoutException::class,
    errorMessage: 'Payment confirmation timed out',
);

entityType accepts any EntityTypeContract (typically a backed enum published into your app). actorType is a free string — conventionally user, system, cron, or job. retentionDays defaults to 360 and can be overridden per call via ActivityLogContext::forActor(..., retentionDays: 90).

Automatic Model Logging (the ActivityLoggable trait)

Add the trait to an Eloquent model to log created / updated / deleted automatically with a computed diff:

use Illuminate\Database\Eloquent\Model;
use Tsitsishvili\ElasticAudit\Traits\ActivityLoggable;

class Order extends Model
{
    use ActivityLoggable;

    // Optional — defaults to Str::snake(class_basename($model)), e.g. "order"
    protected string $activityEntityType = 'order';

    // Optional — fields excluded from the diff.
    // Defaults to ['created_at', 'updated_at', 'deleted_at'] when not defined.
    protected array $activityLogExcept = ['updated_at', 'created_at'];

    // Optional — if non-empty, only these fields appear in the diff.
    protected array $activityLogOnly = [];
}
Eloquent event Action logged changes content
created {entity}.created {field: {old: null, new: value}} for all logged attributes
updated {entity}.updated {field: {old, new}} for dirty fields only
deleted {entity}.deleted {} (the entity itself is the event)

$activityLogOnly is applied first (whitelist), then $activityLogExcept (blacklist). The entity id is (string) $model->getKey().

Actor Resolution

The trait resolves the current actor automatically:

  1. Auth::check() is true → actorType: "user", actorId: Auth::id()
  2. Otherwise → actorType: "system", actorId: null

For manual ActivityLog::record() calls you set the actor explicitly via the context.

Document Shape

{
  "@timestamp": "2026-06-04T10:00:00Z",
  "event_id": "01JX...",
  "schema_version": 1,
  "request_id": "01JX...",
  "actor":   { "type": "user", "id": 42 },
  "action":  "order.status_updated",
  "entity":  { "type": "order", "id": "99" },
  "changes": { "status": { "old": "pending", "new": "paid" } },
  "metadata": { "ip": "1.2.3.4" },
  "success": true,
  "error":   { "class": null, "message": null },
  "retention_days": 360
}

changes and metadata are stored but not indexed (enabled: false) — their keys are caller-defined, so they are searchable by event_id/action/actor/entity but not by their inner keys.

Activity Dashboard

When activity_logs.dashboard.enabled is true, the dashboard is served under the configured path (default /logger/activity):

  • Overview — total / success / failure counts, top actions, top actor types.
  • List — paginated, newest first, filterable by action, actor type, success, entity id, and date range.
  • Detail — full event, a before/after change table, and a metadata dump.

Access is gated by the same authorization callback as the HTTP dashboard:

use Tsitsishvili\ElasticAudit\Dashboard\Dashboard;

Dashboard::auth(fn ($request) => $request->user()?->can('viewActivityLogs') === true);

By default (no callback registered) access is restricted to the local environment.

Pruning Activity Logs

php artisan activity-logs:prune

Deletes documents older than their own retention_days value (each document carries its retention, so different actions can have different lifetimes). Schedule it daily.

Guarantees

  • Capture never throws. A logging failure can never break the surrounding request — errors are swallowed and the job's own failures are logged, not propagated.
  • Disabled is a true no-op. With activity_logs.enabled = false, record() returns immediately and no job is dispatched.
  • Backward compatibility. The indexed document shape is versioned via ActivityLogData::SCHEMA_VERSION; the mapping is dynamic: strict.

Internal Versioning

Use Git tags as Composer versions.

git tag v1.0.0
git push origin v1.0.0

Recommended policy:

  • Patch: bug fixes only, for example v1.0.1
  • Minor: backward-compatible features, for example v1.1.0
  • Major: breaking config, contract, class, or behavior changes, for example v2.0.0

Applications should depend on stable tags:

{
  "require": {
    "tsitsishvili/elastic-audit": "^1.0"
  }
}

Avoid using dev-main in production applications.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: proprietary
  • 更新时间: 2026-06-24