Logging Setup for Frontend + Backend Observability in a Nomad-Hosted App

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
Verified by ExactMetrics