
September 11, 2026
12 min read
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.
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.
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.
| Field | Type | Purpose | Example |
|---|---|---|---|
timestamp | ISO-8601 UTC | Ordering across time zones | 2026-09-11T14:03:22Z |
level | string | Severity filter | error, warning, info |
message | string | Human-readable summary | Payment callback failed |
request_id | UUID | Trace one HTTP request | 8f3c… |
service | string | Identify app in multi-service setups | notary-api |
environment | string | Separate prod from staging | production |
user_id | int|null | Actor context when authenticated | 1042 |
duration_ms | int | Performance signals | 842 |
exception | object | Class, message, stack on errors | see 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
- debug — verbose internals; off in production unless sampling.
- info — normal business events: order placed, email sent.
- warning — recoverable oddities: retry succeeded, deprecated API used.
- error — user-visible failure or missed SLA.
- 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.
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.
Structured logging vs plain text: which approach wins?
Use both layers wisely. Developers read messages during local work. Machines read JSON in production.
| Criterion | Plain text | Structured JSON |
|---|---|---|
| Human readability in terminal | Excellent | Good with pretty-print |
| Search and aggregation | Poor | Excellent |
| Alerting on field values | Fragile regex | Native |
| Storage cost at scale | Lower per line | Higher; mitigated by sampling |
| Setup effort | Zero | Moderate one-time schema work |
| Compliance audit trails | Hard to prove | Field-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
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.

