Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Development Guides/Logging Standards

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

  1. Overview
  2. Log Levels
  3. Configuration
  4. Usage Patterns
  5. Activity Logging
  6. Sensitive Data
  7. Best Practices

Overview

Laravel uses the Monolog library for logging. Logs help with debugging, monitoring, auditing, and troubleshooting issues in development and production.

Log Channels

ChannelPurposeEnvironment
stackDefault multi-channelAll
singleSingle log fileLegacy
dailyDaily rotating files (default)All
stderrStandard error outputDocker/containers
slackCritical alertsProduction

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):

LevelPriorityUsage
emergency0System is unusable
alert1Action must be taken immediately
critical2Critical conditions
error3Runtime errors
warning4Exceptional occurrences that are not errors
notice5Normal but significant events
info6Interesting events
debug7Detailed 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