Two Lines of Code That Fix Logging Spaghetti in Real JavaScript Systems
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
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:
console.log('updating contact', contactId);Is fine — until you see it 10,000 times a day and have no idea which one matters.

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.

The Entire Setup (Yes, Really)
Here’s what it actually takes to switch your app over.
At your application entry point:
import { replaceConsole, loggerMiddleware } from '@purpleowl-io/tracepack';
replaceConsole();That’s line one.
Then, after your auth middleware and before your routes:
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:
contact createdAfter:
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:
function updateContact(data) {
console.log('updating contact', data.id);
}The output automatically becomes:
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.

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

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:
node app.js | jq 'select(.txId == "abc-789")'Or:
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:
npm i @purpleowl-io/tracepackKeywords
Related Articles
Replacing the Legacy App That Runs Your Business (Without Burning the Place Down)
Is your legacy system holding your business hostage? Learn a practical, step-by-step approach to rep...
ERP for Small Businesses: What Actually Matters
Confused about ERP? This guide cuts through the jargon to reveal what ERP software *actually* does f...
Build or Buy Your ERP? You're Asking the Wrong Question
Stop debating build vs. buy for your ERP. Discover how to identify the operational edge you need and...