hsr-engineer / observability-laravel
Audit trail package for Laravel using OpenTelemetry, Loki, and Tempo
Installs: 12
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/hsr-engineer/observability-laravel
Requires
- php: ^8.2
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/log: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- monolog/monolog: ^3.0
- open-telemetry/api: ^1.0
- open-telemetry/context: ^1.0
- open-telemetry/exporter-otlp: ^1.0
- open-telemetry/sdk: ^1.0
Requires (Dev)
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0|^11.0
README
Complete observability solution for Laravel applications with OpenTelemetry tracing, structured logging, and direct Loki integration.
✨ Features
- 🔍 Automatic Tracing - Root span per request with W3C TraceContext & B3 propagation
- 📝 Structured Logging - Consistent JSON schema with automatic trace_id/span_id injection
- 🚀 Direct Loki Push - Send logs directly to Loki without Promtail
- 🔧 Auto-Instrumentation - HTTP client, database queries, queue jobs
- 🔒 Sensitive Data Masking - Automatic masking of passwords, tokens, secrets
- 💪 Fail-Open Design - Won't crash your app if observability fails
- ⚡ Octane/Swoole Safe - Request-scoped context, no global state leaks
- 🎯 Fluent API - Developer-friendly API for audit trails
📋 Requirements
- PHP 8.2+
- Laravel 10.x, 11.x, or 12.x
📦 Installation
composer require hsr/observability-laravel
Publish configuration:
php artisan vendor:publish --tag=observability-config
🚀 Quick Start
1. Environment Configuration
# Service identification OTEL_SERVICE_NAME=my-service APP_ENV=production # OTLP endpoint (Tempo, Jaeger, or OTel Collector) OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 # Optional: Direct Loki push LOKI_ENDPOINT=http://loki:3100/loki/api/v1/push # Feature flags OBSERVABILITY_TRACING_ENABLED=true OBSERVABILITY_LOGGING_ENABLED=true
2. Configure Logging Channel
Add to config/logging.php:
<?php return [ 'default' => env('LOG_CHANNEL', 'observability'), 'channels' => [ // Option 1: Using the custom channel factory (RECOMMENDED for file/stdout) 'observability' => [ 'driver' => 'custom', 'via' => \HsrObservability\Logging\ObservabilityLogChannelFactory::class, 'stream' => 'php://stdout', // or storage_path('logs/observability.log') 'level' => env('LOG_LEVEL', 'debug'), ], // Option 2: Direct push to Loki (NO PROMTAIL NEEDED!) 'loki' => [ 'driver' => 'custom', 'via' => \HsrObservability\Logging\LokiLogChannelFactory::class, 'endpoint' => env('LOKI_ENDPOINT', 'http://loki:3100/loki/api/v1/push'), 'labels' => [ 'app' => env('APP_NAME', 'laravel'), 'env' => env('APP_ENV', 'production'), ], 'batch_size' => 1, // 1 = immediate send 'timeout' => 5, 'level' => env('LOG_LEVEL', 'debug'), ], // Option 3: Using monolog driver with tap 'observability-tap' => [ 'driver' => 'monolog', 'handler' => \Monolog\Handler\StreamHandler::class, 'with' => [ 'stream' => 'php://stdout', ], 'tap' => [\HsrObservability\Logging\ObservabilityLogTap::class], ], // Stack for multiple outputs (e.g., stdout + Loki) 'stack' => [ 'driver' => 'stack', 'channels' => ['observability', 'loki'], 'ignore_exceptions' => false, ], // ... other channels ], ];
Important: After adding the package, run:
composer dump-autoload php artisan config:clear
3. Verify Installation
php artisan observability:test
Usage
Basic Logging (Auto-Enriched)
use Illuminate\Support\Facades\Log; // All logs automatically get trace_id, span_id, service info, etc. Log::info('User logged in'); Log::error('Payment failed', ['order_id' => 'ORD-123']);
Using Fluent Context API
use HsrObservability\Facades\Observability; class UserController extends Controller { public function store(Request $request) { $user = User::create($request->validated()); // Set observability context for audit trail Observability::ctx() ->module('USER') ->action('CREATE') ->category('DATA_CHANGE') ->risk('MEDIUM') ->change( fieldsChanged: ['name', 'email'], old: [], new: ['name' => $user->name, 'email' => $user->email] ) ->logInfo('User created successfully'); return response()->json($user, 201); } public function update(Request $request, User $user) { $oldData = $user->only(['name', 'email']); $user->update($request->validated()); Observability::forUpdate('USER', fieldsChanged: array_keys($request->validated()), old: $oldData, new: $user->only(['name', 'email']) )->logInfo('User updated'); return response()->json($user); } public function destroy(User $user) { $userData = $user->toArray(); $user->delete(); Observability::forDelete('USER', $userData) ->risk('HIGH') ->logWarning('User deleted'); return response()->noContent(); } }
Convenience Methods
// Create operation Observability::forCreate('ORDER', ['total' => 1000, 'items' => 5]) ->logInfo('Order placed'); // Update operation Observability::forUpdate('ORDER', ['status'], old: ['status' => 'pending'], new: ['status' => 'paid'] )->logInfo('Order status updated'); // View operation (low risk, minimal context) Observability::forView('ORDER') ->logDebug('Order viewed'); // Auth operations Observability::forAuth('LOGIN') ->logInfo('User authenticated'); Observability::forAuth('LOGOUT') ->logInfo('User logged out');
Error Handling
try { // risky operation } catch (\Exception $e) { Observability::ctx() ->module('PAYMENT') ->action('CHARGE') ->category('TRANSACTION') ->errorFromException($e) ->risk('HIGH') ->logError('Payment failed'); throw $e; }
Custom Context
Observability::ctx() ->module('REPORT') ->action('GENERATE') ->extra('report_type', 'monthly') ->extra('date_range', ['2024-01-01', '2024-01-31']) ->extras([ 'format' => 'pdf', 'recipients' => 5, ]) ->logInfo('Report generated');
Log Output Schema
All logs follow this exact JSON schema:
{
"timestamp": 1761277589,
"environment": "production",
"trace_id": "5b8892f1-1e6d-4494-b725-1e7042355808",
"span_id": "e3c5a6c1c9d04777",
"service": {
"name": "CORE"
},
"user": {
"id": "USER-ID",
"role_name": "Administrator",
"name": "Aldi"
},
"context": {
"module": "USER",
"action": "CREATE",
"category": "DATA_CHANGE",
"http_status_code": 200,
"latency_ms": 123,
"error_code": null,
"error_message": null,
"risk_level": "MEDIUM",
"level": "INFO"
},
"request": {
"method": "POST",
"path": "/api/v1/users",
"query": {
"include": "roles"
}
},
"response": {
"body": null
},
"change": {
"fields_changed": ["name", "email"],
"old_value": {
"name": null,
"email": null
},
"new_value": {
"name": "Example Name",
"email": "example@hsr.com"
}
},
"client": {
"ip_address": "120.120.10.1",
"browser_name": "Chrome",
"browser_version": "126.0.6478.61"
},
"location": {
"city": "Bandung",
"region": "Jawa Barat",
"country": "ID",
"timezone": "Asia/Jakarta"
}
}
Configuration
Full configuration options in config/observability.php:
return [ // Service identification 'service' => [ 'name' => env('OTEL_SERVICE_NAME', 'laravel-app'), 'version' => env('APP_VERSION', '1.0.0'), 'environment' => env('APP_ENV', 'production'), ], // Tracing configuration 'tracing' => [ 'enabled' => env('OBSERVABILITY_TRACING_ENABLED', true), 'exporter' => [ 'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://otel-collector:4318'), ], 'sampling' => [ 'ratio' => env('OTEL_TRACES_SAMPLER_ARG', 1.0), ], ], // Logging configuration 'logging' => [ 'enabled' => env('OBSERVABILITY_LOGGING_ENABLED', true), 'json_schema_enabled' => env('OBSERVABILITY_JSON_SCHEMA', true), 'include_request_query' => true, 'include_request_body' => false, // Enable with caution 'include_response_body' => false, // Enable with caution 'max_body_size_bytes' => 2048, ], // Sensitive data masking 'masking' => [ 'enabled' => true, 'replacement' => '[REDACTED]', 'fields' => [ 'password', 'token', 'api_key', 'secret', 'authorization', 'cookie', 'otp', 'pin', 'cvv', ], ], // Routes to ignore 'ignored_routes' => [ 'healthz', 'health', 'metrics', ], ];
Promtail/Loki Configuration
Label Guidelines (Anti-High-Cardinality)
Safe labels (low cardinality):
service→ service.nameenv→ environmentlevel→ context.levelcategory→ context.category
DO NOT use as labels (high cardinality):
trace_id→ query as JSON fieldspan_id→ query as JSON fielduser_id→ query as JSON fieldrequest_path→ query as JSON field
Example Promtail Config
scrape_configs: - job_name: laravel static_configs: - targets: [localhost] labels: job: laravel __path__: /var/log/laravel/*.log pipeline_stages: - json: expressions: level: context.level service: service.name env: environment category: context.category - labels: level: service: env: category:
Loki Queries
# Find logs by trace_id
{service="my-service"} | json | trace_id="5b8892f1-1e6d-4494-b725-1e7042355808"
# Find errors for a user
{service="my-service", level="ERROR"} | json | user_id="USER-123"
# Find data changes
{service="my-service", category="DATA_CHANGE"} | json | context_module="USER"
OpenTelemetry Collector Setup
docker-compose.yml
services: otel-collector: image: otel/opentelemetry-collector-contrib:latest command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "4318:4318" # OTLP HTTP tempo: image: grafana/tempo:latest command: ["-config.file=/etc/tempo.yaml"] volumes: - ./tempo.yaml:/etc/tempo.yaml ports: - "3200:3200"
otel-collector-config.yaml
receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 1000 exporters: otlp/tempo: endpoint: tempo:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/tempo]
Testing
# Run tests composer test # Run with coverage composer test-coverage # Static analysis composer analyse
Troubleshooting
Self-Test Command
php artisan observability:test
Common Issues
1. No traces appearing in Tempo
- Check OTLP endpoint is reachable:
curl http://otel-collector:4318/v1/traces - Verify
OBSERVABILITY_TRACING_ENABLED=true - Check sampling ratio is > 0
2. Missing trace_id in logs
- Ensure TracingMiddleware is registered
- Check if route is in
ignored_routes - Verify logging processor is added to channel
3. High memory usage
- Reduce
max_body_size_bytes - Disable
include_request_bodyandinclude_response_body - Increase sampling ratio to reduce trace volume
License
MIT