Logging Standards Guide
Last Updated: 2026-01-09 Status: Active Audience: Developers
This guide documents logging standards and best practices for the Client Portal application.
Table of Contents
Overview
Laravel uses the Monolog library for logging. Logs help with debugging, monitoring, auditing, and troubleshooting issues in development and production.
Log Channels
| Channel | Purpose | Environment |
|---|---|---|
stack | Default multi-channel | All |
single | Single log file | Legacy |
daily | Daily rotating files (default) | All |
stderr | Standard error output | Docker/containers |
slack | Critical alerts | Production |
Note: This project uses LOG_STACK=daily by default, creating daily log files named laravel-YYYY-MM-DD.log.
Log Levels
Laravel supports standard syslog levels (in order of severity):
| Level | Priority | Usage |
|---|---|---|
emergency | 0 | System is unusable |
alert | 1 | Action must be taken immediately |
critical | 2 | Critical conditions |
error | 3 | Runtime errors |
warning | 4 | Exceptional occurrences that are not errors |
notice | 5 | Normal but significant events |
info | 6 | Interesting events |
debug | 7 | Detailed debug information |
When to Use Each Level
use Illuminate\Support\Facades\Log;
// EMERGENCY: System is completely down
Log::emergency('Database server is unreachable', ['host' => $host]);
// ALERT: Immediate action required
Log::alert('Disk space critically low', ['available' => '100MB']);
// CRITICAL: Application component failed
Log::critical('Payment gateway connection failed', ['gateway' => 'stripe']);
// ERROR: Runtime errors that don't require immediate action
Log::error('Failed to send invoice email', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage(),
]);
// WARNING: Exceptional occurrences that are not errors
Log::warning('API rate limit approaching', ['remaining' => 10]);
// NOTICE: Normal but significant events
Log::notice('User password changed', ['user_id' => $user->id]);
// INFO: Interesting events
Log::info('Invoice created', ['invoice_id' => $invoice->id]);
// DEBUG: Detailed debug information (development only)
Log::debug('Query executed', ['sql' => $query, 'bindings' => $bindings]);
Configuration
Basic Configuration
// config/logging.php
return [
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
],
];
Production Configuration
Production should emit machine-parseable JSON so logs can be ingested by aggregation tooling (Sentry, CloudWatch, etc.).
# .env (production) — file-based JSON, rotated daily
LOG_CHANNEL=json
LOG_LEVEL=warning
# .env (production, containerized) — JSON to stderr
LOG_CHANNEL=json-stderr
LOG_LEVEL=warning
The json and json-stderr channels (defined in config/logging.php) use Monolog's JsonFormatter. Each line is a complete JSON object containing the message, level, channel, timestamp, exception details (when applicable), and any shared context. The AddCorrelationId middleware injects correlation_id into every entry via Log::shareContext() — so a single request's logs can be traced through the stack.
Sample line (formatted across multiple lines here for readability — in practice it is one line):
{
"message": "Invoice marked paid",
"context": {
"invoice_id": 4821,
"client_id": 17,
"correlation_id": "01HXG4-..."
},
"level": 200,
"level_name": "INFO",
"channel": "production",
"datetime": "2026-05-20T19:43:12.103Z",
"extra": {}
}
Local development should keep LOG_CHANNEL=stack (line-formatted text) for human readability.
Custom Channels
// config/logging.php
'channels' => [
// ... existing channels
'activity' => [
'driver' => 'daily',
'path' => storage_path('logs/activity.log'),
'level' => 'info',
'days' => 30,
],
'security' => [
'driver' => 'daily',
'path' => storage_path('logs/security.log'),
'level' => 'warning',
'days' => 90,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Client Portal',
'emoji' => ':boom:',
'level' => 'critical',
],
],
Usage Patterns
Basic Logging
use Illuminate\Support\Facades\Log;
// Simple message
Log::info('User logged in');
// With context data
Log::info('User logged in', [
'user_id' => $user->id,
'email' => $user->email,
'ip' => request()->ip(),
]);
// Logging exceptions
try {
$this->processPayment($invoice);
} catch (\Exception $e) {
Log::error('Payment processing failed', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
Logging to Specific Channels
// Log to specific channel
Log::channel('security')->warning('Failed login attempt', [
'email' => $email,
'ip' => request()->ip(),
]);
// Log to multiple channels
Log::stack(['daily', 'slack'])->critical('Payment gateway down');
Contextual Logging
// Add context to all subsequent logs
Log::withContext([
'request_id' => request()->header('X-Request-ID'),
'user_id' => auth()->id(),
]);
// All logs in this request will include request_id and user_id
Log::info('Processing order');
Log::info('Order completed');
Logging in Controllers
class InvoiceController extends Controller
{
public function store(StoreInvoiceRequest $request)
{
$invoice = Invoice::create($request->validated());
Log::info('Invoice created', [
'invoice_id' => $invoice->id,
'client_id' => $invoice->client_id,
'total' => $invoice->total,
'user_id' => auth()->id(),
]);
return redirect()->route('admin.invoices.show', $invoice);
}
}
Logging in Services
class PaymentService
{
public function processPayment(Invoice $invoice, array $paymentData): bool
{
Log::info('Processing payment', [
'invoice_id' => $invoice->id,
'amount' => $invoice->total,
]);
try {
// Process payment...
Log::info('Payment successful', [
'invoice_id' => $invoice->id,
'transaction_id' => $transactionId,
]);
return true;
} catch (PaymentException $e) {
Log::error('Payment failed', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage(),
'code' => $e->getCode(),
]);
return false;
}
}
}
Activity Logging
The application uses a custom ActivityLog model for tracking user actions:
Activity Log Model
// app/Models/ActivityLog.php
class ActivityLog extends Model
{
protected $fillable = [
'user_id',
'action',
'subject_type',
'subject_id',
'properties',
'ip_address',
'user_agent',
];
protected function casts(): array
{
return [
'properties' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function subject(): MorphTo
{
return $this->morphTo();
}
}
Logging Activities
// app/Services/ActivityLogService.php
class ActivityLogService
{
public function log(string $action, ?Model $subject = null, array $properties = []): ActivityLog
{
return ActivityLog::create([
'user_id' => auth()->id(),
'action' => $action,
'subject_type' => $subject ? get_class($subject) : null,
'subject_id' => $subject?->id,
'properties' => $properties,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
}
// Usage
$activityLogger->log('created', $invoice, [
'total' => $invoice->total,
'items_count' => $invoice->items->count(),
]);
$activityLogger->log('status_changed', $invoice, [
'from' => 'draft',
'to' => 'sent',
]);
Activity Trait for Models
// app/Traits/LogsActivity.php
trait LogsActivity
{
protected static function bootLogsActivity(): void
{
static::created(function (Model $model) {
app(ActivityLogService::class)->log('created', $model);
});
static::updated(function (Model $model) {
app(ActivityLogService::class)->log('updated', $model, [
'changes' => $model->getChanges(),
]);
});
static::deleted(function (Model $model) {
app(ActivityLogService::class)->log('deleted', $model);
});
}
}
// Usage in model
class Invoice extends Model
{
use LogsActivity;
}
Sensitive Data
Never Log These
- Passwords (plaintext or hashed)
- API keys and tokens
- Credit card numbers
- Social security numbers
- Personal health information
- Session tokens
Redacting Sensitive Data
// Bad - Logs password
Log::info('User registration', ['email' => $email, 'password' => $password]);
// Good - Omit sensitive data
Log::info('User registration', ['email' => $email]);
// Good - Redact if needed
Log::info('API request', [
'endpoint' => $endpoint,
'api_key' => str_repeat('*', strlen($apiKey) - 4) . substr($apiKey, -4),
]);
OAuth Responses and External API Bodies — Required
Any code that logs an OAuth token response, the body of a failed HTTP call to a third-party API, or any payload that may contain credentials MUST route it through App\Support\Logging\OAuthLogSanitizer before logging.
use App\Support\Logging\OAuthLogSanitizer;
// OAuth token response (e.g. Google, LinkedIn token exchange)
if (isset($token['error'])) {
Log::error('OAuth token error', [
'oauth_error' => $token['error'] ?? null,
'oauth_error_description' => $token['error_description'] ?? null,
'token_response' => OAuthLogSanitizer::sanitizeArray($token),
]);
}
// HTTP response body of a failed external call
if (! $response->successful()) {
Log::error('Third-party API call failed', [
'status' => $response->status(),
'body' => OAuthLogSanitizer::sanitizeBody($response->body()),
]);
}
sanitizeArray() is case-insensitive, recurses into nested arrays, and redacts these keys by default: access_token, refresh_token, id_token, client_secret, authorization, code, password, secret, api_key, apikey, token. Pass additional names via the $extraKeys parameter for provider-specific fields.
sanitizeBody() parses JSON bodies and redacts sensitive keys; non-JSON bodies return null in production and a truncated preview only when APP_DEBUG=true.
This pattern is enforced — never log a raw OAuth token array, $response->body(), or $response->json() directly. Any new integration that fails this rule will not pass code review.
Masking Helpers
// app/Helpers/LogHelper.php
class LogHelper
{
public static function maskEmail(string $email): string
{
$parts = explode('@', $email);
$name = substr($parts[0], 0, 2) . str_repeat('*', max(0, strlen($parts[0]) - 2));
return $name . '@' . $parts[1];
}
public static function maskCard(string $number): string
{
return str_repeat('*', strlen($number) - 4) . substr($number, -4);
}
}
// Usage
Log::info('Payment processed', [
'email' => LogHelper::maskEmail($user->email), // jo***@example.com
'card' => LogHelper::maskCard($cardNumber), // ************4242
]);
Best Practices
Do
- Use appropriate log levels
- Include relevant context data (IDs, counts, states)
- Log at operation boundaries (start/end of processes)
- Use structured data (arrays) not string concatenation
- Include request/correlation IDs for tracing
- Log exceptions with stack traces
- Rotate logs in production
// Good - Structured with context
Log::info('Order processing started', [
'order_id' => $order->id,
'items_count' => $order->items->count(),
'total' => $order->total,
]);
// Bad - String concatenation
Log::info("Processing order {$order->id} with {$order->items->count()} items");
Don't
- Log sensitive data (passwords, tokens, PII)
- Log inside tight loops (aggregate instead)
- Use debug level in production
- Log every database query (use query logging sparingly)
- Forget to include identifying information
// Bad - Logging in loop
foreach ($items as $item) {
Log::info('Processing item', ['item_id' => $item->id]);
}
// Good - Log aggregated
Log::info('Processing batch', [
'items_count' => count($items),
'item_ids' => collect($items)->pluck('id')->toArray(),
]);
Log Entry Template
Log::info('Action description', [
// What
'action' => 'invoice_created',
// Who
'user_id' => auth()->id(),
// What entity
'invoice_id' => $invoice->id,
'client_id' => $invoice->client_id,
// Relevant data
'total' => $invoice->total,
'items_count' => $invoice->items->count(),
// When (automatic in logs, but useful for async)
'timestamp' => now()->toIso8601String(),
]);
Viewing Logs
Admin Log Viewer
The application includes a built-in log viewer for administrators:
- URL:
/admin/system/logs - Features:
- View logs from any daily log file
- Filter by log level (Error, Warning, Info, Debug)
- Click to expand full log messages
- Copy log entries to clipboard
- Defaults to today's log file
Development
# Tail today's log in real-time
tail -f storage/logs/laravel-$(date +%Y-%m-%d).log
# Search logs
grep "invoice_id" storage/logs/laravel-*.log
# Clear logs
php artisan log:clear
Laravel Telescope (Development)
Install Laravel Telescope for a web-based log viewer:
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
Production Monitoring
Consider integrating with:
- Laravel Forge logs
- AWS CloudWatch
- Papertrail
- Bugsnag/Sentry for error tracking