Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Structured Logging Best Practices

By Kokil Thapa | Last reviewed: September 2026

Your payment webhook failed at 2 a.m. The error log says "Something went wrong." You grep the file, find forty similar lines, and still cannot tell which user or order broke. Structured logging best practices fix that by writing machine-readable JSON events with stable fields instead of free-form strings. On production Laravel and PHP applications I maintain, structured logs cut mean time to diagnosis sharply because every line carries request ID, user context, and error codes you can filter in one query.

What is structured logging and why does it matter in production?

Structured logging writes each event as a key-value record, usually JSON. A human still reads the message field. Search tools read everything else.

Plain text logs force regex gymnastics. Structured logs let you ask precise questions: show all 502 errors for order checkout in the last hour where gateway equals Khalti. That query is trivial in centralized logging with the ELK stack or any JSON-aware platform.

In my experience working on production Laravel applications, the shift happens when debugging stops being file-by-file archaeology. You attach a correlation ID at the edge and follow one request from controller through queue job to payment callback.

Plain Text vs Structured LoggingPlain Text Log[2026-09-11] ERROR: failedNo order_id, no traceJSON Structured Loglevel, order_id, trace_idFilterable in one queryManual grepSlow, fragileHours to debugCentral searchKibana, Loki, CloudWatchMinutes to root cause
Structured logging best practices replace ambiguous strings with JSON fields your observability stack can index and filter.

The payoff shows up during incidents, compliance reviews, and ongoing application maintenance. Auditors ask who accessed a document. Support asks why a booking confirmation never sent. Structured context answers both without redeploying debug code.

When unstructured logs are still acceptable

Local development and one-off CLI scripts can stay human-readable. Production paths that touch money, identity, or legal documents should not. On a legal-tech portal I built, document upload failures needed case ID and file hash in every event. Plain strings hid that data inside sentences parsers could not split reliably.

How do you implement structured logging in Laravel and PHP?

PHP standardised logging through PSR-3 Logger Interface. Laravel 12 and 13 ship Monolog underneath. You do not need a new framework. You need consistent context passed on every call.

Start with Laravel's logging stack. Configure a JSON formatter on your default channel, then enrich context globally through middleware and queue hooks.

Step 1: Enable JSON output in config/logging.php

<?php
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;

return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'json_stdout' => [
            'driver' => 'monolog',
            'handler' => StreamHandler::class,
            'with' => [
                'stream' => 'php://stdout',
            ],
            'formatter' => JsonFormatter::class,
            'formatter_with' => [
                'append_newline' => true,
                'ignore_empty_context' => true,
            ],
            'level' => env('LOG_LEVEL', 'debug'),
        ],
    ],
];

On Ubuntu servers I deploy to, stdout JSON plays nicely with Docker logging drivers and systemd journal shipping. One process writes events. The platform collects them.

Step 2: Bind request context in middleware

<?php
namespace App\Http\Middleware;

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

class AssignRequestContext
{
    public function handle(Request $request, Closure $next)
    {
        $requestId = $request->header('X-Request-Id') ?? (string) Str::uuid();

        Log::shareContext([
            'request_id' => $requestId,
            'method' => $request->method(),
            'path' => $request->path(),
            'ip_hash' => hash('sha256', $request->ip() ?? ''),
        ]);

        $response = $next($request);
        $response->headers->set('X-Request-Id', $requestId);

        return $response;
    }
}

Register that middleware early in your HTTP stack. Every controller, job dispatch, and exception handler inherits the same request_id without repeating boilerplate. This pattern mirrors what I use on booking platforms with queue-heavy workflows.

Step 3: Log with context arrays, not string concatenation

Log::info('Payment callback received', [
    'gateway' => 'khalti',
    'order_id' => $order->id,
    'amount_paisa' => $payload['amount'],
    'status' => $payload['status'],
]);

Bad practice embeds variables inside the message: Log::error("Payment failed for order {$order->id}"). The order ID becomes unindexed prose. Keep messages stable. Put variables in context.

Structured Log PipelineLaravel AppPSR-3 Log callsMiddlewarerequest_idMonologJsonFormatterstdoutone JSON lineLog shipper: Fluent Bit, Filebeat, or CloudWatch agentSearch: OpenSearch, Loki, Datadog, GrafanaFilter by request_id, level, gateway
A typical structured logging pipeline: enrich at the app layer, serialize as JSON, ship off-box, search centrally.

For Symfony 8.1 projects, the idea is identical. Inject LoggerInterface, pass context arrays, configure a JSON formatter on your Monolog handler. Framework syntax differs. Discipline does not.

Validate JSON shape during CI. Paste sample log lines into the JSON formatter tool to confirm parsers accept them before you ship a release.

What fields should every structured log event include?

Consistency beats completeness. Pick a schema, document it, and enforce it in code review. Random field names destroy searchability faster than missing fields.

I recommend a baseline envelope plus optional domain blocks. The envelope travels on every event. Domain blocks appear where relevant.

FieldTypePurposeExample
timestampISO-8601 UTCOrdering across time zones2026-09-11T14:03:22Z
levelstringSeverity filtererror, warning, info
messagestringHuman-readable summaryPayment callback failed
request_idUUIDTrace one HTTP request8f3c…
servicestringIdentify app in multi-service setupsnotary-api
environmentstringSeparate prod from stagingproduction
user_idint|nullActor context when authenticated1042
duration_msintPerformance signals842
exceptionobjectClass, message, stack on errorssee below

Domain-specific context blocks

eCommerce events should carry order_id, cart_id, and gateway. Legal-tech portals benefit from case_ref and document_type without logging file contents. API endpoints need route_name, status_code, and client_id when OAuth clients call in.

On client portals with document sharing, I log upload outcomes with file size and MIME type. I never log raw PDF bytes or citizen ID numbers. The field list is short on purpose.

Log levels: use them deliberately

  1. debug — verbose internals; off in production unless sampling.
  2. info — normal business events: order placed, email sent.
  3. warning — recoverable oddities: retry succeeded, deprecated API used.
  4. error — user-visible failure or missed SLA.
  5. critical — data loss risk or total subsystem down.

A common mistake is logging every ORM query at info in production. Noise drowns signal and inflates storage bills. Rs 3,000/month (~USD 22) in log volume adds up across sister sites on shared infrastructure.

How do you keep sensitive data out of structured logs?

Structured logging makes exfiltration easier if you dump secrets into indexed fields. JSON in Elasticsearch is searchable by everyone with dashboard access. Treat logs like a database table with public read replicas.

Never log passwords, API keys, full card numbers, JWT tokens, or national ID values. Hash or truncate identifiers when the full value is not required for debugging.

Redact at the formatter layer

<?php
namespace App\Logging;

use Monolog\LogRecord;

class RedactSensitiveProcessor
{
    private array $keys = [
        'password', 'token', 'authorization',
        'pan', 'citizenship_no', 'card_number',
    ];

    public function __invoke(LogRecord $record): LogRecord
    {
        $context = $this->scrub($record->context);
        $extra = $this->scrub($record->extra);

        return $record->with(context: $context, extra: $extra);
    }

    private function scrub(array $data): array
    {
        foreach ($data as $key => $value) {
            if (in_array(strtolower((string) $key), $this->keys, true)) {
                $data[$key] = '[REDACTED]';
            }
        }
        return $data;
    }
}

Register the processor on your Monolog channel in a service provider. Central redaction beats hoping every developer remembers.

Also scrub exception messages that echo SQL with embedded emails. Wrap external API failures and log response status plus a safe error code, not the full raw body.

Log Field Redaction DecisionNew log field?Is it a secret or PII?YesNoRedact or hashNever index raw valueLog with contextAdd to schema docsAudit quarterlyShip to central store
Redaction belongs in the logging pipeline, not in post-incident panic after secrets already hit disk.

Security reviews should include log samples. The OWASP-aligned Laravel security guide covers broader hardening, but logging leaks belong on the same checklist as SQL injection.

How do you collect and search structured logs in production?

Writing JSON to storage/logs/laravel.log on one VM is a start. It is not observability. Production needs off-box shipping, retention policy, and indexed search.

Single-server Laravel on Ubuntu

Many Nepal client projects run on a single Ubuntu 22 or 24 box with Apache and PHP-FPM 8.3 or 8.4. Install Filebeat or Fluent Bit. Point it at stdout if you containerise, or at the JSON log file if you must.

output.elasticsearch:
  hosts: ["https://logs.internal:9200"]
  index: "laravel-%{+yyyy.MM.dd}"

processors:
  - decode_json_fields:
      fields: ["message"]
      target: "app"
  - drop_fields:
      fields: ["agent", "ecs"]

Rotate and compress local files even when shipping remotely. Full disks have taken down more sites than bad code in deployments I have cleaned up.

Multi-service and queue workers

Queue workers are separate processes. They do not inherit HTTP middleware context unless you pass it. When dispatching a job, include request_id in the payload or use Laravel's contextual logging in the job handler.

public function handle(): void
{
    Log::shareContext([
        'request_id' => $this->requestId,
        'job' => static::class,
    ]);

    Log::info('Generating itinerary PDF', [
        'booking_id' => $this->bookingId,
    ]);
}

Without that step, your web logs and worker logs cannot be joined. Incidents look like two unrelated failures.

For broader platform design, read modern Laravel architecture patterns and Laravel API best practices. Both assume you can trace requests end to end.

Correlation with OpenTelemetry

OpenTelemetry logs are converging traces, metrics, and log records under shared trace IDs. You do not need full OTel on day one. You do need one ID propagated from HTTP through queues to outbound webhooks. That single field unlocks most incident workflows.

Correlation ID Across ServicesHTTP Requestreq: abc-123Queue Jobreq: abc-123Webhookreq: abc-123Single search: request_id = abc-12314:03:01 POST /checkout14:03:02 job SendReceipt14:03:04 gateway callback timeout
Structured logging best practices pay off when the same correlation ID appears in web, worker, and webhook logs.

Structured logging vs plain text: which approach wins?

Use both layers wisely. Developers read messages during local work. Machines read JSON in production.

CriterionPlain textStructured JSON
Human readability in terminalExcellentGood with pretty-print
Search and aggregationPoorExcellent
Alerting on field valuesFragile regexNative
Storage cost at scaleLower per lineHigher; mitigated by sampling
Setup effortZeroModerate one-time schema work
Compliance audit trailsHard to proveField-level proof

Verdict: adopt structured JSON for production application logs. Keep plain text for local default channels if your team prefers it. Switch channels through LOG_CHANNEL in .env per environment, the same way CI/CD secrets management separates staging from production credentials.

Operational habits that compound value

  • Document your log schema in the repo README or ADR folder.
  • Add a CI check that sample log output parses as valid JSON.
  • Set retention: 30 days hot, 90 days warm is a sane default for SMB apps.
  • Create saved searches for top failure modes: payment, auth, upload.
  • Review log volume monthly; drop debug fields nobody queries.

Wire logging into deploy pipelines. After symlink swap on Deployer releases, tail JSON stdout for five minutes. I do this on sister sites sharing GitLab CI and zero-downtime deploys. A bad formatter config shows up immediately, not after a client complaint.

If you run WordPress 7.1 or WooCommerce 11.1 alongside Laravel services, apply the same discipline. Plugin errors in PHP error logs should still carry request context when you control the code path.

For API-heavy products, pair structured logs with the guidance in REST API design best practices. Consistent error codes in responses and logs shorten support cycles.

Performance work also benefits. When optimising slow endpoints, duration_ms in structured logs beats guessing from Apache access logs alone.

Key Takeaways

  • Emit JSON log events with stable fields—request_id, level, service, environment—on every production code path.
  • Keep messages constant; put variables in context arrays via PSR-3, not string interpolation.
  • Redact secrets and PII in a Monolog processor before logs touch disk or Elasticsearch.
  • Propagate correlation IDs from HTTP through queue jobs to payment webhooks.
  • Ship logs off the app server with Filebeat or Fluent Bit and search them centrally.
  • Document your schema, validate JSON in CI, and review volume so signal stays high.

People Also Ask

Is structured logging the same as JSON logging?

JSON is the most common serialization format for structured logs, but structure is the goal and JSON is one means. You could use key=value pairs or logfmt. JSON wins because every observability stack parses it natively and Laravel's Monolog JsonFormatter is built in.

Does structured logging slow down Laravel applications?

The overhead is small compared to database and network I/O. JsonFormatter costs microseconds per event. Unbounded debug logging at info level hurts more than JSON encoding. Tune log levels and sample verbose paths in high-traffic endpoints.

What is the best log level for production Laravel apps?

Set LOG_LEVEL=warning or info in production depending on traffic. Use info for business events you alert on. Reserve debug for local and staging. Critical payment flows deserve explicit info lines even when global level is warning.

How long should I retain structured application logs?

Thirty days of searchable retention covers most incident investigations. Keep ninety days in cheaper storage if compliance requires it. Legal and financial workflows may need longer archival; define that with stakeholders before you pay for infinite hot storage.

Ship observability your next developer will thank you for

Structured logging best practices are not a luxury for large teams. They are baseline hygiene for any PHP 8.3+ or Laravel 12 application that handles money, bookings, or client documents. Start with JSON formatting, middleware context, redaction, and one correlation ID. Expand into centralized search when a single grep across production files stops scaling.

If you want help auditing logs on an existing app or designing observability into a new build, see the API development services page or browse the project portfolio. For related reading, start with build pipeline automation and Ubuntu server security hardening. When you are ready to implement this on your stack, contact us and we can review your current logging setup together.

Frequently Asked Questions

Structured logging writes machine-readable JSON events with consistent fields—timestamp, level, message, and correlation ID—instead of free-form strings that search tools cannot index or filter reliably.

Emit JSON log events with stable envelope fields—request_id, level, service, environment—via PSR-3 loggers on every production path. Keep message strings constant and pass variables in context arrays, not string interpolation. Redact passwords, tokens, and national IDs in a Monolog processor before logs hit disk. Propagate correlation IDs from HTTP through queue jobs to payment webhooks. Ship logs off the app server with Filebeat or Fluent Bit into a searchable central store like Elasticsearch instead of leaving JSON scattered across local files.

Laravel ships Monolog underneath PSR-3. In config/logging.php, add a monolog channel with JsonFormatter pointed at php://stdout or your log file. Register middleware early that calls Log::shareContext with request_id, method, path, and a hashed IP on every HTTP request, and return the ID in the X-Request-Id response header. Log with context arrays: Log::info('Payment callback received', ['gateway' => 'khalti', 'order_id' => $order->id]). Validate sample output parses as JSON in CI before release.

Consistency beats completeness—pick a schema and enforce it in code review. Every event needs an envelope: ISO-8601 UTC timestamp, level, stable message string, UUID request_id, service name, environment, optional user_id, duration_ms for performance, and an exception object on errors. Add domain blocks where relevant: order_id and gateway for eCommerce, case_ref and document_type for legal-tech portals, route_name and status_code for APIs. Never log raw file contents or citizen ID numbers even when context fields exist.

JSON in Elasticsearch is searchable by everyone with dashboard access—treat logs like a database with public read replicas. Never log passwords, API keys, JWT tokens, full card numbers, or national ID values. Register a Monolog processor such as RedactSensitiveProcessor that scrubs keys like password, token, authorization, pan, citizenship_no, and card_number to [REDACTED] before records touch disk. Also truncate exception messages that echo SQL with embedded emails, and log external API failures as status plus safe error code—not the full raw response body.

Many Nepal client projects run Laravel on one Ubuntu 22 or 24 box with Apache and PHP-FPM 8.3 or 8.4. Install Filebeat or Fluent Bit, point it at stdout if containerised or at the JSON log file otherwise, and ship to Elasticsearch with a daily index pattern like laravel-%{+yyyy.MM.dd}. Use decode_json_fields so message content becomes searchable app fields. Rotate and compress local files even when shipping remotely—full disks have taken down more sites than bad code in deployments I have cleaned up.

Use both layers wisely. Plain text excels for human readability in local terminals; structured JSON wins for search, aggregation, field-based alerting, and compliance audit trails in production. Plain text forces fragile regex; JSON lets you ask precise questions like all 502 checkout errors where gateway equals Khalti in the last hour. Setup effort is moderate one-time schema work versus zero for plain text, but structured logging cuts mean time to diagnosis sharply during incidents. Switch channels through LOG_CHANNEL in .env per environment.

Queue workers are separate processes—they do not inherit HTTP middleware context unless you pass it explicitly. When dispatching a job, include request_id in the payload, then call Log::shareContext in the job handler with that ID and the job class name before logging business events. Without that step, web logs and worker logs look like two unrelated failures during incidents. The same correlation ID should also appear in outbound payment webhook logs. You do not need full OpenTelemetry on day one, but one propagated ID unlocks most incident workflows.

Set LOG_LEVEL to warning or info in production depending on traffic. Use info for business events you alert on; reserve debug for local and staging only.

The overhead is small compared to database and network I/O. JsonFormatter costs microseconds per event. What actually hurts performance is unbounded debug logging at info level on high-traffic endpoints—not JSON encoding itself. A common mistake is logging every ORM query at info in production; that noise drowns signal and inflates storage bills. Tune log levels deliberately: debug for verbose internals off in production unless sampling, info for normal business events, warning for recoverable oddities, error for user-visible failures, critical for data loss risk.

JSON is the most common serialization format for structured logs, but structure is the goal and JSON is one means. You could use key=value pairs or logfmt instead. JSON wins because every observability stack parses it natively and Laravel's Monolog JsonFormatter is built in. The distinction matters when planning your pipeline: structured logging means consistent, indexable fields your team documents and enforces in code review. JSON is simply how Laravel and most ELK-stack deployments serialize those fields for Elasticsearch and similar platforms.

A sane default for SMB applications is 30 days hot and 90 days warm retention. Hot storage keeps recent events quickly searchable for active incident response and support tickets. Warm storage holds enough history for compliance questions—who accessed a document, why a booking confirmation never sent—without paying Elasticsearch prices on every byte forever. Review log volume monthly and drop debug fields nobody queries. Document your retention policy alongside the log schema in your repo README or ADR folder so auditors and new developers know what to expect.

On shared infrastructure, Rs 3,000/month (~USD 22) in log volume adds up quickly across multiple sites—often from logging every ORM query at info level.

The discipline matches Laravel even though syntax differs. Symfony 8.1 projects inject LoggerInterface from PSR-3, pass context arrays on every call instead of embedding variables in message strings, and configure a JSON formatter on the Monolog handler. Keep messages stable; put order IDs, gateway names, and error codes in context. Validate JSON shape during CI by pasting sample log lines into a JSON formatter tool to confirm parsers accept them before shipping a release. Framework syntax differs; the structured logging best practices do not.

Embedding variables inside message strings with string interpolation makes IDs unindexed prose—use context arrays instead. Skipping request_id propagation into queue jobs splits one incident across unrelated log streams. Logging secrets or PII into JSON fields that Elasticsearch indexes permanently. Running debug or ORM query logging at info in production creates noise and storage cost. Writing JSON only to storage/logs/laravel.log on one VM without Filebeat or Fluent Bit shipping off-box is a start, not observability. Finally, random field names in context destroy searchability faster than missing fields—document and enforce one schema.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: