Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Logging That Helps at 3 AM: Structured Logs in Laravel

About Post

It's 3 AM. A tenant says their payment went through but the app shows it as unpaid. You open the logs and find this:

[2025-07-26 02:47:13] production.ERROR: Something went wrong
[2025-07-26 02:47:13] production.ERROR: Payment failed
[2025-07-26 02:47:14] production.INFO: here

Which payment? Which user? Which request? Is "here" related? You have logs, technically. What you don't have is information.

Good logging isn't about logging more. It's about writing each line for the tired person who'll search for it later. Usually that person is you.

Rule 1: the message is a label, the context is the data

Laravel's logger takes a second argument, a context array. Use it every time:

// Before
Log::error("Payment failed for invoice {$invoice->id}");

// After
Log::error('Payment gateway declined charge', [
    'invoice_id' => $invoice->id,
    'user_id' => $invoice->user_id,
    'amount' => $invoice->amount,
    'gateway_code' => $response->json('error.code'),
    'attempt' => $attempt,
]);

Two things changed. The message is now a fixed string, so you can search for every occurrence of "Payment gateway declined charge" across all invoices. And the variable parts are separate fields, so a log tool can filter by invoice_id or count failures by gateway_code. That's what "structured" means.

Rule 2: give every request an ID

Under load, lines from twenty different requests interleave. A request ID lets you pull out exactly the lines that belong to one request. Laravel 11 introduced the Context facade, which makes this easy: anything you add to it is automatically attached to every log entry for the rest of the request.

use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;

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

        Context::add('request_id', $requestId);

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

        return $response;
    }
}

Register it as global middleware in bootstrap/app.php with $middleware->prepend(AssignRequestId::class). Returning the ID in a response header is a small, lovely bonus: when a user reports an error, support can ask for the ID (or your mobile app can show it on the error screen) and you go straight to the right lines.

The best part: Context data also travels into queued jobs dispatched during that request. So the job that sends the receipt email logs with the same request_id as the request that created the payment. Add the user ID too, once they're authenticated, and every line answers "who?" for free.

Rule 3: levels are a promise about urgency

If everything is an error, nothing is. I think of levels as "who needs to care, and how soon":

LevelMeaningExample
debugUseful while developing, off in productionQuery timings, payload shapes
infoNormal business events worth a recordContract signed, payment received
warningSomething odd, handled, worth watchingRetrying a gateway call, slow response
errorSomething failed and a person may need to actPayment callback couldn't be processed
criticalWake someone upCan't reach the database, queue is stuck

A validation failure is not an error. A user typing a wrong password is not an error. If they show up as errors, real errors drown, and alerts become noise people learn to ignore.

Rule 4: log the decision, not the trail

"Entered function", "here", "here 2": these were useful for five minutes while debugging and useless forever after. The lines worth keeping record decisions and outcomes: why a payment was rejected, why a job skipped a record, which branch a webhook took and why.

A good test: if this line appeared in an incident, would it tell you something you couldn't get from the database?

Rule 5: never log secrets (it's easier than you think)

The most common leak isn't someone logging a password on purpose. It's logging a whole request or response "just to see it":

// Risky: may contain passwords, tokens, card details
Log::info('Incoming request', $request->all());

// Better: pick what you need
Log::info('Profile update requested', $request->only(['name', 'phone']));

Watch out for Authorization headers, API responses that echo tokens back, and full model arrays ($hidden on a model only affects serialisation, so think before dumping attributes). For stack traces, PHP 8.2's #[\SensitiveParameter] attribute redacts a parameter's value from backtraces.

The 3 AM test: before you write a log line, ask "what will I search for when this goes wrong?" Put that in the context array. If the answer is "nothing", don't write the line.

Rule 6: write JSON when a machine will read it

Plain text is fine when you read logs with tail. Once logs go to CloudWatch or any log platform, JSON lets you query fields directly. A custom channel in config/logging.php:

'json' => [
    'driver' => 'monolog',
    'level' => env('LOG_LEVEL', 'info'),
    'handler' => Monolog\Handler\StreamHandler::class,
    'handler_with' => ['stream' => storage_path('logs/laravel.json')],
    'formatter' => Monolog\Formatter\JsonFormatter::class,
],

Combine channels with the stack driver: JSON to a file or stderr for everything, plus a Slack or email channel at critical level only. The logging docs cover all the drivers.

Bonus: let exceptions carry their own context

Custom exceptions in Laravel can define a context() method that returns an array. Laravel adds it to the log entry when the exception is reported, so a PaymentDeclined exception can always log its invoice and gateway code without every catch block remembering to.

Checklist

  • Fixed messages, variable data in the context array.
  • A request ID on every line, via middleware and Context, returned in a header.
  • Levels that mean something; errors are for things a person must act on.
  • Decisions and outcomes, not breadcrumbs.
  • No secrets, no full request dumps.
  • JSON once a machine reads your logs.

What's the worst log line you've ever had to debug with? I've seen plenty of "here" over the years, and at least once I'm fairly sure I wrote it.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close