Beginner

Structured Logging in Laravel

Configure Laravel's logging system for structured JSON output. Ship logs to LogFlow using a custom Monolog handler with automatic batching.

LogFlow TeamAugust 24, 202610 minLaravelPHP

Laravel uses Monolog under the hood, which gives you powerful logging capabilities out of the box. This tutorial configures it for structured JSON output and ships logs to LogFlow.

Prerequisites

  • Laravel 10+ project
  • PHP 8.1+
  • A LogFlow account (free tier works)
  • Your API key from Settings → API Key

Step 1 — Configure JSON Output

Edit config/logging.php to add a JSON-formatted channel:

'channels' => [
    // ... existing channels

    'logflow' => [
        'driver' => 'custom',
        'via' => App\Logging\LogFlowLogger::class,
        'level' => env('LOG_LEVEL', 'info'),
    ],

    'json_stdout' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\StreamHandler::class,
        'with' => [
            'stream' => 'php://stdout',
        ],
        'formatter' => Monolog\Formatter\JsonFormatter::class,
    ],

    'stack' => [
        'driver' => 'stack',
        'channels' => ['json_stdout', 'logflow'],
        'ignore_exceptions' => false,
    ],
],

Set the default channel in .env:

LOG_CHANNEL=stack
LOGFLOW_API_KEY=lf_your_api_key_here

Step 2 — Create the LogFlow Logger

Create app/Logging/LogFlowLogger.php:

<?php

namespace App\Logging;

use Monolog\Logger;
use App\Logging\LogFlowHandler;

class LogFlowLogger
{
    public function __invoke(array $config): Logger
    {
        $logger = new Logger('logflow');
        $logger->pushHandler(new LogFlowHandler(
            apiKey: config('services.logflow.api_key', env('LOGFLOW_API_KEY', '')),
            service: config('app.name', 'laravel'),
            level: $config['level'] ?? 'info',
        ));
        return $logger;
    }
}

Create app/Logging/LogFlowHandler.php:

<?php

namespace App\Logging;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;
use Monolog\Level;
use Illuminate\Support\Facades\Http;

class LogFlowHandler extends AbstractProcessingHandler
{
    private string $apiKey;
    private string $service;
    private array $buffer = [];
    private int $batchSize = 50;

    public function __construct(
        string $apiKey,
        string $service = 'laravel',
        string $level = 'info',
    ) {
        parent::__construct(Level::fromName($level));
        $this->apiKey = $apiKey;
        $this->service = $service;

        // Flush on shutdown
        register_shutdown_function([$this, 'flush']);
    }

    protected function write(LogRecord $record): void
    {
        $this->buffer[] = [
            'timestamp' => $record->datetime->format('Y-m-d H:i:s.v'),
            'level' => strtolower($record->level->name),
            'message' => $record->message,
            'service' => $this->service,
            ...$record->context,
        ];

        if (count($this->buffer) >= $this->batchSize) {
            $this->flush();
        }
    }

    public function flush(): void
    {
        if (empty($this->buffer) || empty($this->apiKey)) {
            return;
        }

        $batch = $this->buffer;
        $this->buffer = [];

        try {
            Http::withToken($this->apiKey)
                ->timeout(5)
                ->post('https://api.getlogflow.com/v1/logs', $batch);
        } catch (\Throwable) {
            // Never crash the app because of logging
        }
    }
}

Step 3 — Add Request Context Middleware

Create app/Http/Middleware/LogContext.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;

class LogContext
{
    public function handle(Request $request, Closure $next)
    {
        $requestId = $request->header('X-Request-ID', Str::uuid()->toString());
        $startTime = microtime(true);

        // Make request ID available to all log calls
        Log::shareContext([
            'request_id' => $requestId,
            'ip' => $request->ip(),
        ]);

        $response = $next($request);

        $durationMs = round((microtime(true) - $startTime) * 1000);

        // Log completed request
        Log::info('request.handled', [
            'method' => $request->method(),
            'path' => $request->path(),
            'status' => $response->status(),
            'duration_ms' => $durationMs,
            'user_id' => $request->user()?->id,
        ]);

        // Add request ID to response
        $response->header('X-Request-ID', $requestId);

        return $response;
    }
}

Register in app/Http/Kernel.php (or bootstrap/app.php for Laravel 11):

// Laravel 10
protected $middleware = [
    // ... existing middleware
    \App\Http\Middleware\LogContext::class,
];

// Laravel 11+
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\LogContext::class);
})

Step 4 — Log Application Events

Use Laravel's logger with structured context throughout your code:

use Illuminate\Support\Facades\Log;

// Business events
Log::info('order.created', [
    'order_id' => $order->id,
    'user_id' => $order->user_id,
    'total' => $order->total,
    'items_count' => $order->items->count(),
]);

// Errors with exception context
try {
    $this->processPayment($order);
} catch (PaymentException $e) {
    Log::error('payment.failed', [
        'order_id' => $order->id,
        'error' => $e->getMessage(),
        'gateway' => $e->getGateway(),
    ]);
    throw $e;
}

// Queue job logging
class ProcessOrder implements ShouldQueue
{
    public function handle(): void
    {
        Log::info('job.started', ['order_id' => $this->orderId]);
        
        // ... process
        
        Log::info('job.completed', [
            'order_id' => $this->orderId,
            'duration_ms' => $this->elapsed(),
        ]);
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('job.failed', [
            'order_id' => $this->orderId,
            'error' => $exception->getMessage(),
            'attempt' => $this->attempts(),
        ]);
    }
}

Step 5 — Track Database Queries (Optional)

Log slow database queries for performance debugging:

// In AppServiceProvider::boot()
DB::listen(function ($query) {
    if ($query->time > 100) { // Log queries over 100ms
        Log::warning('query.slow', [
            'sql' => Str::limit($query->sql, 200),
            'duration_ms' => $query->time,
            'connection' => $query->connectionName,
        ]);
    }
});

Step 6 — Verify in LogFlow

Run your Laravel app and make some requests:

php artisan serve
curl http://localhost:8000/
curl http://localhost:8000/api/users

Open LogFlow and search:

service:laravel

You should see structured request logs with path, status, duration, and user context.

Next Steps

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free