Laravel observability toolkit with critical control points, structured logging, performance timing, and trace context.

v0.1.1 2025-06-19 15:38 UTC

This package is auto-updated.

Last update: 2025-06-19 15:40:43 UTC


README

Laravel Supported Versions MIT Licensed Latest Version on Packagist Application Testing Static Analysis Code Style

Laravel Monitor is an observability helper / toolkit for Laravel applications.

This package is active development and its API can change abruptly without any notice. Please reach out if you plan to use it in a production environment.

Table of Contents

Installation

Install via Composer:

composer require kirschbaum-development/monitor

Publish configuration files:

php artisan vendor:publish --tag="monitor-config"

Components

Structured Logging

What it does: Enhances Laravel's logging with automatic enrichment (trace IDs, timing, memory usage, structured context) and smart origin resolution from class namespaces.

use Kirschbaum\Monitor\Facades\Monitor;

// In App\Http\Controllers\Api\UserController
class UserController extends Controller
{
    public function login(LoginRequest $request)
    {
        // Automatic origin resolution from full namespace
        Monitor::log($this)->info('User login attempt', [
            'email' => $request->email,
            'ip' => $request->ip()
        ]);
    }
}

// In App\Services\Payment\StripePaymentService  
class StripePaymentService
{
    public function processPayment($amount)
    {
        // Origin automatically resolved to clean, readable format
        Monitor::log($this)->info('Processing payment', [
            'amount' => $amount,
            'processor' => 'stripe'
        ]);
    }
}

Note: While you can override with Monitor::log('CustomName'), using log($this) is preferred as it automatically provides meaningful, consistent origin tracking from your actual class structure.

What it logs:

{
    "level": "info",
    "event": "Monitor:Http:Controllers:Api:UserController:info",
    "message": "[Monitor:Http:Controllers:Api:UserController] User login attempt",
    "trace_id": "9d2b4e8f-3a1c-4d5e-8f2a-1b3c4d5e6f7g",
    "context": {
        "email": "[REDACTED]",
        "ip": "192.168.1.1"
    },
    "timestamp": "2024-01-15T14:30:45.123Z",
    "duration_ms": 245,
    "memory_mb": 45.23
}

Note: The event field uses the raw origin name (after path replacers but before wrapper), while the message field uses the wrapped origin name for readability.

Configuration: Origin path replacers, separators, and wrappers control how class names appear in logs:

// config/monitor.php
'origin_path_replacers' => [
    'App\\' => 'Monitor\\',                  // Default: Replace App\ with Monitor\
    // 'App\\Http\\Controllers\\' => '',     // Example: Remove controller namespace
    // 'App\\Services\\Payment\\' => 'Pay\\', // Example: Shorten payment services
    // 'App\\Services\\' => 'Svc\\',         // Example: General service shortening
],
'origin_separator' => ':',           // App\Http\Controllers\Api\UserController → Monitor:Http:Controllers:Api:UserController  
'origin_path_wrapper' => 'square',   // Monitor:Http:Controllers:Api:UserController → [Monitor:Http:Controllers:Api:UserController]

Controlled Execution Blocks

What it does: Monitors critical operations with automatic start/end logging, exception-specific handling, DB transactions, circuit breakers, and true escalation for uncaught exceptions.

Note: The second parameter $origin (usually $this) is optional and automatically provides origin context to the structured logger used by the controlled block, eliminating the need for a separate ->log() call.

Factory & Execution

use Kirschbaum\Monitor\Facades\Monitor;

// Create and execute controlled block
$result = Monitor::controlled('payment_processing', $this)
    ->run(function() {
        return processPayment($data);
    });

Context Management

/*
 * Adds additional context to the structured logger.
 */
Monitor::controlled('payment_processing', $this)
    ->addContext([
        'transaction_id' => 'txn_456',
        'gateway' => 'stripe'
    ]);

/*
 * Will completely replace structured logger context.
 * ⚠️ Not recommended unless you have a good reason to do so.
 */
Monitor::controlled('payment_processing', $this)
    ->overrideContext([
        'user_id' => 123,
        'operation' => 'payment',
        'amount' => 99.99
    ]);

Exception Handling

Exception-Specific Handlers (catching):

Monitor::controlled('payment_processing', $this)
    ->catching([
        DatabaseException::class => function($exception, $meta) {
            $cachedData = ExampleModel::getCachedData();
            return $cachedData; // Recovery value
        },
        NetworkException::class => function($exception, $meta) {
            $this->exampleRetryLater($meta);
            // No return = just handle, don't recover
        },
        PaymentException::class => function($exception, $meta) {
            $this->exampleNotifyFinanceTeam($exception, $meta);
            throw $exception; // Re-throw if needed
        },
        // Other exception types remain uncaught.
    ])

Uncaught Exception Handling (onUncaughtException):

Monitor::controlled('payment_processing', $this)
    ->onUncaughtException(function($exception, $meta) {
        // Example actions, the exception will remain uncaught
        $this->alertOpsTeam($exception, $meta);
        $this->sendToErrorTracking($exception);
    })

Key Behavior:

  • Only specified exception types in catching() are handled
  • Handlers can return recovery values to prevent re-throwing
  • onUncaughtException() only fires for exceptions not caught by catching() handlers
  • True separation between expected (caught) and unexpected (uncaught) failures

Circuit Breaker & Database Protection

What are Circuit Breakers? Circuit breakers prevent cascading failures by temporarily stopping requests to a failing service, allowing it time to recover. They automatically "open" after a threshold of failures and "close" once the service is healthy again, protecting your application from wasting resources on operations likely to fail.

Monitor::controlled('payment_processing', $this)
    ->withCircuitBreaker('payment_gateway', 3, 60) // 3 failures, 60s timeout
    ->withDatabaseTransaction(2, [DeadlockException::class], [ValidationException::class])

Circuit Breaker HTTP Middleware

You can also protect entire routes or route groups using the CheckCircuitBreakers middleware:

// bootstrap/app.php or register as route middleware
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'circuit' => \Kirschbaum\Monitor\Http\Middleware\CheckCircuitBreakers::class,
    ]);
})

// In your routes
Route::middleware(['circuit:payment_gateway,external_api'])
    ->group(function () {
        Route::post('/payments', [PaymentController::class, 'store']);
        Route::get('/external-data', [DataController::class, 'fetch']);
    });

// Or on individual routes
Route::get('/api/data')
    ->middleware('circuit:slow_service')
    ->name('data.fetch');

Circuit Breaker Middleware Features:

  • Multiple Breakers: Check multiple circuit breakers with circuit:breaker1,breaker2,breaker3
  • Graceful Degradation: Returns HTTP 503 (Service Unavailable) when circuit is open
  • Standard Headers: Includes Retry-After, X-Circuit-Breaker, and X-Circuit-Breaker-Status headers
  • Jitter Protection: Built-in randomized retry delays prevent thundering herd effects
  • Auto-Recovery: Circuits automatically close when services recover

Response Headers When Circuit is Open:

HTTP/1.1 503 Service Unavailable
Retry-After: 45
X-Circuit-Breaker: payment_gateway
X-Circuit-Breaker-Status: open

The Retry-After header includes intelligent jitter - instead of all clients retrying at the exact same time, it provides a random delay between 0 and the remaining decay time, preventing overwhelming the recovering service.

Tracing & Logging

Monitor::controlled('payment_processing', $this)
    ->overrideTraceId('custom-trace-12345')
    // Origin is automatically set from the second parameter ($this)

Complete Example

class PaymentService
{
    public function processPayment($amount, $userId)
    {
        return Monitor::controlled('payment_processing', $this)
            ->addContext([
                'user_id' => $userId,
                'amount' => $amount,
                'currency' => 'USD'
            ])
            ->withCircuitBreaker('payment_gateway', 3, 120)
            ->withDatabaseTransaction(1, [DeadlockException::class])
            ->catching([
                PaymentDeclinedException::class => function($e, $meta) {
                    return ['status' => 'declined', 'reason' => $e->getMessage()];
                },
                InsufficientFundsException::class => function($e, $meta) {
                    return ['status' => 'insufficient_funds'];
                }
            ])
            ->onUncaughtException(fn($e, $meta) => SomeEscalationLogic::run($e, $meta))
            ->run(function() use ($amount) {
                return $this->chargeCard($amount);
            });
    }
}

What it logs:

Success:

{"message": "[Monitor:Services:PaymentService] STARTED", "controlled_block": "payment_processing", "controlled_block_id": "01HK..."}
{"message": "[Monitor:Services:PaymentService] ENDED", "status": "ok", "duration_ms": 1250}

Caught Exception (Recovery):

{"message": "[Monitor:Services:PaymentService] STARTED", "controlled_block": "payment_processing"}
{"message": "[Monitor:Services:PaymentService] CAUGHT", "exception": "PaymentDeclinedException", "duration_ms": 500}
{"message": "[Monitor:Services:PaymentService] RECOVERED", "recovery_value": "array"}

Uncaught Exception (Escalation):

{"message": "[Monitor:Services:PaymentService] STARTED", "controlled_block": "payment_processing"}
{"message": "[Monitor:Services:PaymentService] UNCAUGHT", "exception": "RuntimeException", "uncaught": true, "duration_ms": 300}

API Reference

Method Purpose Returns
Monitor::controlled(string $name, string|object $origin = null) Create controlled block with optional origin self
->overrideContext(array $context) Replace entire context self
->addContext(array $context) Merge additional context self
->catching(array $handlers) Define exception-specific handlers self
->onUncaughtException(Closure $callback) Handle uncaught exceptions only self
->withCircuitBreaker(string $name, int $threshold, int $decay) Configure circuit breaker self
->withDatabaseTransaction(int $retries, array $only, array $exclude) Wrap in DB transaction with retry self
->overrideTraceId(string $traceId) Set custom trace ID self
->run(Closure $callback) Execute the controlled block mixed

Distributed Tracing

What it does: Provides correlation IDs that follow requests across services, jobs, and operations.

use Kirschbaum\Monitor\Facades\Monitor;

class OrderController extends Controller
{
    public function store()
    {
        // Start trace (typically via middleware)
        Monitor::trace()->start();
        
        Monitor::log($this)->info('Processing order');
        
        // All subsequent operations share the same trace ID
        $this->paymentService->charge($amount);
        
        // Queue job with trace context
        ProcessOrderJob::dispatch($order);
    }
}

class PaymentService
{
    public function charge($amount)
    {
        // Automatically includes trace ID from OrderController
        Monitor::log($this)->info('Charging card', ['amount' => $amount]);
    }
}

Trace Management:

// Manual control
Monitor::trace()->start();            // Generate new UUID (throws if already started)
Monitor::trace()->override($traceId); // Use specific ID (overwrites existing)
Monitor::trace()->pickup($traceId);   // Start if not started, optionally with specific ID
Monitor::trace()->id();               // Get current ID (throws if not started)
Monitor::trace()->hasStarted();       // Check if active
Monitor::trace()->hasNotStarted();    // Check if not active

Key Differences:

  • start() - Throws exception if trace already exists
  • override() - Always sets trace ID, replacing any existing one
  • pickup() - Safe method that starts only if not already started

HTTP Middleware

What it does: Automatically manages trace IDs for HTTP requests, enabling seamless distributed tracing across services.

Registration:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\Kirschbaum\Monitor\Http\Middleware\StartMonitorTrace::class);
})

Behavior:

  • Incoming: Picks up X-Trace-Id header or generates new UUID
  • Outgoing: Sets X-Trace-Id header in response
  • Preserves: Existing traces when already started

Cross-service usage:

// Service A
$response = Http::withHeaders([
    'X-Trace-Id' => Monitor::trace()->id()
])->get('https://service-b.example.com/api/data');

// Service B automatically uses the same trace ID

Configuration: Custom header name via trace_header config or MONITOR_TRACE_HEADER env var.

Performance Timing

What it does: Provides millisecond-precision timing for operations.

use Kirschbaum\Monitor\Facades\Monitor;

class DataProcessor
{
    public function processData()
    {
        $timer = Monitor::time(); // Auto-starts
        
        // Your processing code
        $this->heavyOperation();
        
        $elapsed = $timer->elapsed(); // Milliseconds
        
        Monitor::log($this)->info('Processing complete', [
            'duration_ms' => $elapsed
        ]);
    }
}

Note: All Monitor logging automatically includes duration_ms from service start.

Circuit Breaker Direct Access

What it does: Provides direct access to circuit breaker state management for advanced use cases.

use Kirschbaum\Monitor\Facades\Monitor;

// Check circuit breaker state
$isOpen = Monitor::breaker()->isOpen('payment_gateway');
$state = Monitor::breaker()->getState('payment_gateway');

// Manual state management
Monitor::breaker()->recordFailure('api_service', 300); // Record failure with 300s decay
Monitor::breaker()->recordSuccess('api_service');      // Record success (resets failures)
Monitor::breaker()->reset('api_service');              // Force reset
Monitor::breaker()->forceOpen('api_service');          // Force open state

Usage in Custom Logic:

class ExternalApiService
{
    public function makeRequest()
    {
        if (Monitor::breaker()->isOpen('external_api')) {
            return $this->getCachedResponse();
        }
        
        try {
            $response = $this->performApiCall();
            Monitor::breaker()->recordSuccess('external_api');
            return $response;
        } catch (Exception $e) {
            Monitor::breaker()->recordFailure('external_api', 120);
            throw $e;
        }
    }
}

Log Redactor Direct Access

What it does: Provides direct access to the redactor for custom redaction needs.

use Kirschbaum\Monitor\Facades\Monitor;

// Direct redaction using configured profile
$redactedData = Monitor::redactor()->redact($sensitiveData);

// Custom profile redaction
$redactedData = Monitor::redactor()->redact($sensitiveData, 'strict');

// Example usage
class UserDataProcessor
{
    public function processUserData(array $userData)
    {
        // Redact before logging or storing
        $safeData = Monitor::redactor()->redact($userData);
        
        Monitor::log($this)->info('Processing user data', $safeData);
        
        return $this->process($userData); // Use original for processing
    }
}

Log Redaction

What it does: Automatically scrubs sensitive data from log context using Kirschbaum Redactor to ensure compliance and security while preserving important data.

Configuration: Simple redaction configuration in config/monitor.php:

'redactor' => [
    'enabled' => true,
    'redactor_profile' => 'default', // Uses Kirschbaum Redactor profiles
],

Usage: Redaction is automatically applied to all Monitor log context:

Monitor::log($this)->info('User data', [
    'id' => 123,
    'email' => 'user@example.com',    // → '[REDACTED]' based on profile rules
    'password' => 'secret123',        // → '[REDACTED]' based on profile rules
    'api_token' => 'sk-1234567890abcdef...', // → '[REDACTED]' based on profile rules
    'name' => 'John Doe',             // → 'John Doe' (if allowed by profile)
]);

For detailed redaction configuration, rules, patterns, and profiles, see the Kirschbaum Redactor documentation.

Complete API Reference

The Monitor facade provides access to all monitoring components:

use Kirschbaum\Monitor\Facades\Monitor;

// Structured logging
Monitor::log($origin)->info('message', $context);

// Controlled execution blocks
Monitor::controlled($name, $origin)->run($callback);

// Distributed tracing
Monitor::trace()->start();
Monitor::trace()->pickup($traceId);

// Performance timing
Monitor::time()->elapsed();

// Circuit breaker management
Monitor::breaker()->isOpen($name);

// Log redaction
Monitor::redactor()->redact($data);

All components integrate seamlessly and share trace context automatically when used together.

Configuration

Environment Variables:

# Core settings
MONITOR_ENABLED=true

# Exception tracing (applies to Controlled blocks only)
MONITOR_TRACE_ENABLED=true
MONITOR_TRACE_FULL_ON_DEBUG=true
MONITOR_TRACE_FORCE_FULL_TRACE=false
MONITOR_TRACE_MAX_LINES=15

# Auto-trace console commands
MONITOR_CONSOLE_AUTO_TRACE_ENABLED=true
MONITOR_CONSOLE_AUTO_TRACE_ENABLE_IN_TESTING=false

# HTTP trace header
MONITOR_TRACE_HEADER=X-Trace-Id

# Circuit breaker defaults
MONITOR_CIRCUIT_BREAKER_DECAY_SECONDS=300
MONITOR_CIRCUIT_BREAKER_RETRY_AFTER=300
MONITOR_CIRCUIT_BREAKER_CORS_HEADERS=false

# Log redaction
MONITOR_REDACTOR_ENABLED=true
MONITOR_REDACTOR_PROFILE=default

Logging Channel: Configure a dedicated Monitor logging channel:

// config/logging.php
'channels' => [
    'monitor' => [
        'driver' => 'daily',
        'path' => storage_path('logs/monitor.log'),
        'level' => 'debug',
        'days' => 14,
        'tap' => [
            \Kirschbaum\Monitor\Taps\StructuredLoggingTap::class,
        ],
    ],
],

Output Examples

Structured Log Entry:

{
    "level": "info",
    "event": "Monitor:Http:Controllers:UserController:info", 
    "message": "[Monitor:Http:Controllers:UserController] User login successful",
    "trace_id": "9d2b4e8f-3a1c-4d5e-8f2a-1b3c4d5e6f7g",
    "context": {
        "user_id": 123,
        "ip_address": "192.168.1.1",
        "_redacted": true
    },
    "timestamp": "2024-01-15T14:30:45.123Z",
    "duration_ms": 1245,
    "memory_mb": 45.23
}

Controlled Block Execution:

{"message": "[Monitor:Services:PaymentService] STARTED", "controlled_block": "payment_processing", "controlled_block_id": "01HK4...", "trace_id": "9d2b4e8f..."}
{"message": "[Monitor:Services:PaymentService] ENDED", "controlled_block": "payment_processing", "status": "ok", "duration_ms": 1250}

Failure with Exception:

{
    "message": "[Monitor:Services:PaymentService] UNCAUGHT",
    "controlled_block": "payment_processing", 
    "exception": {
        "class": "RuntimeException",
        "message": "Card declined",
        "file": "/app/PaymentService.php",
        "line": 45,
        "trace": ["...", "..."]
    },
    "duration_ms": 500,
    "uncaught": true
}

Testing

Run the test suite:

vendor/bin/pest

License

The MIT License (MIT). Please see License File for more information.