Introduction
Production outages, silent failures, and performance degradation happen to every team—but they don't have to catch you off guard. Logging and monitoring Laravel: best practices explained covers the strategies we've learned from running high-traffic applications, where knowing what happens in production isn't optional, it's survival. In this guide, we'll walk through channel configuration, JSON aggregation, error tracking services, and real-time performance monitoring so your team can move fast without losing visibility.
Why logging and monitoring matter in Laravel
Before diving into implementation, let's be clear about why this matters. A production bug without logs is a guessing game. A performance spike without monitoring means your users discover the problem before you do. We've seen teams lose hours chasing issues that would have been obvious with proper logging setup. Laravel gives us excellent tools out of the box—the key is using them strategically.
Log channels: stack, daily, stderr
Laravel's logging system is channel-based, meaning you route different types of logs to different destinations. The default channels available to us are foundational, and understanding when to use each one makes a real difference in practice.
The stack channel
The stack channel is Laravel's way of letting you write to multiple channels at once. In your config/logging.php, you'll see something like this:
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
],We use stack in development and staging because it lets us log to both a file and Slack simultaneously. This means critical errors reach the team immediately while we still have disk logs for detailed investigation. The ignore_exceptions flag tells Laravel whether to suppress errors if one channel fails—typically we leave it false so we know if something's wrong with our logging pipeline itself.
The daily channel
For production, the daily channel is usually our default choice:
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'notice'),
'days' => 14,
],This creates a new log file each day, automatically rotating and deleting files older than 14 days. Without this, a high-traffic application generates gigabytes of logs in weeks. In production, we've configured this to only log notice level and above, reducing noise while keeping warnings and errors. You'd set LOG_CHANNEL=daily in your .env file.
The stderr channel
When deploying to Docker or orchestrated environments like Kubernetes, writing to stderr is the right move:
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_FORMAT'),
'with' => [
'stream' => 'php://stderr',
],
],Containerized systems capture stderr and pipe it to centralized logging. We've used this pattern successfully in production—it removes the need to manage rotating log files and integrates natively with ECS, EKS, or Docker Compose setups. Just set LOG_CHANNEL=stderr and LOG_FORMAT=json in your environment.
JSON logs for aggregators
Plain text logs are readable to humans but nearly useless at scale. When you're searching across thousands of requests per second, you need structured data. This is where JSON logging comes in.
Laravel 8+ supports JSON formatting natively. Configure it like this:
// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'sentry'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'notice'),
'days' => 14,
'formatter' => JsonFormatter::class,
],
],Now every log line is valid JSON. Instead of:
[2025-01-15 10:42:33] production.ERROR: Database connection failedYou get:
{"message":"Database connection failed","level":"error","context":{"exception":"PDOException"},"extra":{},"datetime":"2025-01-15T10:42:33.123456Z"}Log aggregators like DataDog, Splunk, CloudWatch, and New Relic parse this JSON automatically. You can query by field, build dashboards, and set alerts. In production, this is non-negotiable. We've caught issues using structured queries like level:error AND route:/api/checkout AND http_status:500 that would've been invisible in text logs.
When writing your own logs, include context:
Log::error('Payment processing failed', [
'order_id' => $order->id,
'user_id' => $user->id,
'amount' => $order->total,
'gateway' => 'stripe',
'error_code' => $exception->getCode(),
]);
// Outputs:
// {"message":"Payment processing failed","context":{"order_id":123,"user_id":456,"amount":99.99,"gateway":"stripe","error_code":"card_declined"}}This context is gold. It lets you filter, group, and correlate events across your entire system. When you're investigating a spike in payment failures, you can immediately see which gateway is problematic and which users are affected.
Sentry or Flare for errors
Log files tell you what happened, but specialized error tracking services give you the full picture: stack traces, release information, user context, and breadcrumbs leading up to the error.
Sentry integration
Sentry is the industry standard. Install it:
composer require sentry/sentry-laravelThen publish the configuration:
php artisan vendor:publish --provider="Sentry\Laravel\ServiceProvider"In your .env:
SENTRY_LARAVEL_DSN=https://your-key@sentry.io/project-id
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE=v1.2.3Sentry automatically captures unhandled exceptions, and you can add custom instrumentation:
use Sentry\Laravel\Facade as Sentry;
try {
processPayment($order);
} catch (PaymentException $e) {
Sentry::captureException($e, [
'tags' => [
'order_type' => $order->type,
'payment_method' => 'stripe',
],
'user' => auth()->user()?->id,
]);
throw $e;
}We've used Sentry in production for three years. The value comes from grouping identical errors, tracking when they were introduced (via releases), and seeing which users hit them. Performance is crucial here—Sentry is async by default, so it won't slow down your requests.
Laravel Flare
If you prefer staying within the Laravel ecosystem, Flare is Laravel's native error tracking service. It's tightly integrated with the framework and includes features like code review and team collaboration:
composer require facade/flare-client-phpConfigure it in config/flare.php with your API key, and it works out of the box. We've found Flare excellent for smaller teams or solo developers because the setup is simpler and the interface is designed specifically for Laravel.
Laravel Pulse for performance overview
Error tracking tells you when things break. Performance monitoring tells you when they're about to. Laravel Pulse is the official solution for this, introduced in Laravel 11.
Install it:
composer require laravel/pulse
php artisan pulse:installPulse collects metrics automatically: request rate, error rate, slow request detection, database query performance, job queue depth, and custom application metrics. Access the dashboard at /pulse:
// config/pulse.php
'recorders' => [
Recorders\Requests::class,
Recorders\SlowRequests::class,
Recorders\SlowQueries::class,
Recorders\SlowJobs::class,
Recorders\Exceptions::class,
],You can record custom metrics for your domain:
use Laravel\Pulse\Facades\Pulse;
Pulse::record('order_processed', 1, [
'status' => $order->status,
'total' => $order->total,
]);
Pulse::record('cache_hit_ratio', $hitRatio, [
'cache' => 'redis',
]);In production, Pulse stores data in Redis (or your configured cache) with automatic TTL, so there's no database bloat. We've used this to catch performance regressions within minutes of deployment—a slow N+1 query that would've affected users for hours gets caught before it matters.
Combine Pulse with your log aggregator and error tracker, and you have three layers of visibility: detailed logs for investigation, error tracking for incidents, and performance metrics for trends.
Common mistakes
- Logging too much in production. If every request generates 50 log lines at debug level, you'll spend more on storage than servers. Use appropriate log levels: debug for development, notice or higher for production.
- Not including context. A log message without context is noise. Always include order IDs, user IDs, gateway names, and error codes. Make your logs queryable.
- Forgetting about log rotation. Plain text logs can consume entire drives if unmanaged. Use daily rotation or compressed archives. Monitor disk usage.
- Sending sensitive data to external services. Don't log passwords, API keys, or PII to Sentry or your aggregator. Use redaction rules in configuration.
- Ignoring slow query logs. A Pulse alert on slow queries is free optimization. We've caught dozens of N+1 issues before they hit production because we paid attention.
- Not aliasing log facades consistently. Use
Logeverywhere, not sometimesLogand sometimes the container. Consistency makes grep easier and prevents surprises.
Key takeaways
- Use the daily channel in production with appropriate log levels to balance detail and storage costs.
- Enable JSON formatting so log aggregators can index and query your logs effectively.
- Always include context in log messages: user IDs, order IDs, external identifiers, and error details.
- Integrate Sentry or Flare for exception tracking; this catches what logs alone might miss.
- Use Laravel Pulse or similar performance monitoring to track request rates, query performance, and job depths.
- Route logs through a stack channel when you need multiple destinations simultaneously.
- Never log sensitive data; use redaction rules if you need to capture request details.
- Combine all three signals—logs, errors, and metrics—for a complete production picture.






Comments
Loading comments…