Configure Laravel's logging system for structured JSON output. Ship logs to LogFlow using a custom Monolog handler with automatic batching.
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.
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
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
}
}
}
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);
})
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(),
]);
}
}
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,
]);
}
});
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.
Free plan available. No credit card required. Up and running in 2 minutes.
Get started freeLogging in Python with LogFlow
Add production-ready structured logging to any Python application and ship logs to LogFlow.
Structured Logging in Flask
Add production-ready structured logging to any Flask app in under 10 minutes.
Logging in AWS Lambda with LogFlow
AWS Lambda logs disappear into CloudWatch. Here's how to ship structured logs to LogFlow instead.