Two Lines of Code That Fix Logging Spaghetti in Real JavaScript Systems

January 20, 2026
6 min read
Alex Radulovic

Tired of unreadable logs? Fix JavaScript logging spaghetti with just two lines of code! Add context to every log automatically using AsyncLocalStorage.

Two Lines of Code That Fix Logging Spaghetti in Real JavaScript Systems

Two Lines of Code That Fix Logging Spaghetti in Real JavaScript Systems

Almost every JavaScript server starts the same way.

You add a few console.log() calls so you can see what’s happening. You deploy. Everything works.

At that stage, logging feels solved. You can watch requests come in, see data move through the system, and confirm that things behave the way you expect. For a while, this is completely fine.

The problem is that this version of “fine” only exists while your system is still small.

As soon as you have multiple users, concurrent requests, background jobs, and operations that span more than a few milliseconds, logging quietly stops being helpful. Nothing breaks — but nothing is understandable anymore.


What Logging Looks Like Right Before It Fails You

In real systems, actions don’t happen in isolation.

A single user action might:

  • create or update several records
  • trigger validations
  • enqueue background work
  • call external APIs
  • retry on failure
  • finish seconds or minutes later

All of that generates logs.

Now multiply that by dozens or hundreds of users doing the same thing at the same time.

Your logs might be full of “useful” messages, but they no longer form a story. You see what happened, but not who it happened to or which execution path it belonged to.

A log like this:

JavaScript
console.log('updating contact', contactId);

Is fine — until you see it 10,000 times a day and have no idea which one matters.

Image 2


The Conversation Every Team Has (and Then Avoids)

Eventually someone says:

“We should really fix logging.”

What that usually means is:

  • passing user IDs everywhere
  • threading request or transaction IDs through layers
  • replacing console.log() with a custom logger
  • touching hundreds of files
  • risking subtle regressions

For small teams — especially the kind of teams we work with at PurpleOwl — that’s not a small task. It’s a refactor with no visible feature payoff, so it gets kicked down the road.

We’ve done that ourselves.

So instead of designing a “proper” logging framework, we imposed a hard constraint:

If adopting this requires rewriting the app, we won’t use it.

Image 3


The Entire Setup (Yes, Really)

Here’s what it actually takes to switch your app over.

At your application entry point:

JavaScript
import { replaceConsole, loggerMiddleware } from '@purpleowl-io/tracepack';

replaceConsole();

That’s line one.

Then, after your auth middleware and before your routes:

JavaScript
app.use(loggerMiddleware());

That’s line two.

You don’t change your existing logs. You don’t update call sites. You don’t teach the team a new API.

From this point on, every console.log() in your app becomes structured, contextual logging.


What Your Logs Look Like After That

Before:

Code
contact created

After:

JSON
1{
2  "ts": "2025-01-15T10:23:01.000Z",
3  "level": "info",
4  "userId": "alex_123",
5  "txId": "abc-789",
6  "msg": "contact created"
7}

Same code. Different outcome.

Now when you see this deeper in your codebase:

JavaScript
function updateContact(data) {
  console.log('updating contact', data.id);
}

The output automatically becomes:

JSON
1{
2  "level": "info",
3  "userId": "alex_123",
4  "txId": "abc-789",
5  "msg": "updating contact",
6  "args": [12345]
7}

You didn’t pass a user ID. You didn’t pass a transaction ID. You didn’t even think about logging.

The context followed the execution for you.

Image 4


Why This Works Without Being Fragile

Under the hood, this uses Node’s AsyncLocalStorage to attach context to the execution path, not to individual function calls.

That context survives:

  • await
  • promises
  • database drivers
  • network calls
  • timers
  • background work

In practical terms, it means that once a request starts, everything it touches can log with the same identity — even if the work fans out across multiple async layers.

This is the missing piece in most logging setups.


Adding Business Context When It Actually Matters

Sometimes user + transaction isn’t enough.

For example, inside a request handler:

JavaScript
import { log } from '@purpleowl-io/tracepack';

log.addContext({ orderId: req.body.id });

From that point forward, every log in that async chain includes orderId.

So when something goes wrong later — maybe during payment, maybe during fulfillment — you don’t just know that it failed. You know which business object was involved.

That’s the difference between logs that are technically correct and logs that are operationally useful.


Background Jobs, Scripts, and Long-Running Work

Real systems don’t stop when the HTTP response is sent.

This logger handles:

  • cron jobs
  • batch imports
  • background workers
  • delayed retries
  • fire-and-forget tasks

For non-HTTP code, you can explicitly establish context:

JavaScript
1import { withContext } from '@purpleowl-io/tracepack';
2
3await withContext(
4  { userId: 'system', txId: 'nightly-job-001' },
5  async () => {
6    console.log('starting batch');
7    await processBatch();
8    console.log('batch complete');
9  }
10);

Those logs still look exactly like request logs — structured, searchable, and correlated.

Image 1


This Isn’t a Logging Platform (On Purpose)

This doesn’t replace Datadog, ELK, or whatever you ship logs into.

It makes them better.

All output is clean JSON, one log entry per line, so you can:

Bash
node app.js | jq 'select(.txId == "abc-789")'

Or:

Bash
node app.js | jq 'select(.userId == "alex_123")'

It’s intentionally boring infrastructure — the kind that quietly saves time when things go wrong.


Why We’re Sharing This

We built this because we were tired of reading logs that couldn’t answer the questions we actually had.

If your system is still small enough that logs are readable by default, that’s great. Enjoy it while it lasts.

If you’ve crossed the line where concurrency and async work have turned debugging into guesswork, this is a small change that pays off every single time something breaks.

You can find more of our thinking — and why we focus so heavily on pragmatic, small-team systems — at 👉 https://purpleowl.io/blog

The logger is available now:

Bash
npm i @purpleowl-io/tracepack

Keywords

javascript loggingnode.js loggingstructured loggingAsyncLocalStoragelogging contextdebugging javascripterror tracking

Related Articles