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

Maintainers

👁 luisdalmolin

Package info

github.com/kirschbaum-development/monitor

pkg:composer/kirschbaum-development/monitor

Statistics

Installs: 1 603

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 3

v0.1.3 2025-07-08 05:34 UTC

Requires

Suggests

None

Provides

None

Conflicts

None

Replaces

None

MIT c98a1c54b85fb1e3dbfa527650666d16f92e6c1c

  • Belisar Hoxholli <belisar.hoxholli.woop@gmail.com>

loggingtracemonitorlaraveltelemetryobservabilitycontrolled


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.