Context
Was neck-deep in CORS errors logging this week.
This came out of necessity — browser errors were vague, Nomad made it hard to inspect logs, and I needed better observability yesterday. So I wired it all up using winston, express-winston, and a few custom formatters. Added rate limiting and context enrichment. Removed all rogue console.log()s. Now logs feel like a dashboard I can trust.
Still not across the finish line with CORS, but now I know exactly where the requests fail. Which is more than I could say a day ago.
Problem
CORS errors in browser environments are notoriously opaque. Hosted behind Nomad, we struggled to get visibility into:
- Whether the request reached the server
- Why the request wasn’t showing up in Nomad logs
Goal
- Figure out why we were getting the CORS problem in the first place
Logging Setup Goals
- Trace CORS logic explicitly with structured logs
- Enable frontend-to-backend error reporting
- Separate loggers by concern
- Format logs for human readability and machine parsing
- Build with winston, express-winston, and standard middleware
- Route everything through a centralized logger for observability in Nomad’s /alloc/logs output
Setup
1. Logger Container with Scoped Loggers
const container = new winston.Container();
container.add('cors', { /* ... */ });
container.add('frontend', { /* ... */ });
container.add('app', { /* ... */ });
container.add('http', { /* ... */ });
export const cors = container.get('cors');
export const frontend = container.get('frontend');
export const app = container.get('app');
export const http = container.get('http');
Each logger uses a scoped prefix ([CORS], [FRONTEND], etc.), with consistent formatting, timestamps, and custom verbosity levels.
2. Frontend Error Reporting Endpoint
POST /log Receives payloads like:
{
level: 'error',
message: 'Error when logging in',
stack: 'AxiosError: ...',
context: { data: ‘some data’, context: 'LoginPage' },
userAgent: navigator.userAgent
}
This hits a backend controller that parses the error and logs it via the [FRONTEND] logger.
No auth required, but it’s behind rate limiting and scoped CORS.
3. CORS Logging + Debugging
A dedicated CORS logger tracks preflight behavior, incoming origins, and request flow.
app.use((req, res, next) => {
cors.info(`Trust Proxy? ${app.get('trust proxy')}`);
cors.info(`IP: ${req.ip}`);
cors.info(`Origin: ${req.headers.origin}`);
next();
});
After .use(corsMiddleware), we also log:
app.use((req, res, next) => {
cors.debug(`Passed CORS for ${req.method} ${req.originalUrl}`);
next();
});
4. Morgan + express-winston for HTTP Logs
[RICH-HTTP] logs track all route hits, timings, and status codes, feeding into morganMiddleware and expressWinston.logger.
const morganMiddleware = morgan(
':method :url :status :res[content-length] - :response-time ms',
{ stream: { write: msg => http.http(msg.trim()) } },
);
5. Middleware Order
Critically tuned!
app.set('trust proxy', true);
app.use(express.json());
// Log CORS setup
app.use(corsLoggerMiddleware); // Trust, IP, Origin
app.options(/^\/.*$/, corsMiddleware);
app.use(corsMiddleware);
app.use(corsPassThroughLogger); // Logs which paths passed CORS
// Frontend error logger endpoint
app.post('/log', frontendLoggerController);
// HTTP & diagnostics routes
app.use(appLogger);
app.use(v1Router);
app.use(v2Router);
Sample Output
2025-06-26 21:53:18 [CORS] DEBUG: Passed CORS for POST /log
2025-06-26 21:53:18 [FRONTEND] DEBUG: Error when logging in
Stack: AxiosError: Request failed with status code 404
at settle (...)
UserAgent: Mozilla/5.0 (...)
Context: {
"data": "some data",
"context": "LoginPage"
}
2025-06-26 21:53:18 [RICH-HTTP] INFO: POST /log 204 3ms
Learnings & Lessons
- Logger ordering matters. CORS logger must run before corsMiddleware to capture the Origin. Frontend error reporting must be fast + resilient — don’t let it throw.
- No rogue console.log() — breaks structured logs.
Outcome
With this logging setup in place and after a lot more investigation, we figured out that the load balancer was rejecting client requests because a header wasn’t in the load balancer’s allowed headers list.
A quick refactor of the client request in the FE fixed the CORS problem and we finally saw client requests in Nomad logs. Yeehaw!
The logging infrastructure is finally exactly where I want it:
✅ Server-side logs with clearly scoped loggers ([CORS], [APP], [FRONTEND], [RICH-HTTP])
✅ Timestamps, request info, and stack traces — all structured, readable, and consistent
✅ A dedicated endpoint for piping frontend issues to the backend, with user context + user agent
Future Work
- Ship logs to centralized log aggregation (e.g. AWS)
- Add correlation IDs to trace logs across services
- Improve CORS failure clarity (esp. for 403 OPTIONS responses)
- Anonymizing/sanitizing sensitive fields from frontend logs