hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Logging and Observability with @hectoday/http

Why Logging

  • console.log Is Not Enough
  • What to Log
  • Project Setup

Structured Logging

  • JSON Logs
  • Log Levels
  • Building a Logger

Request Logging

  • Request IDs
  • Request-Response Logging
  • Error Logging

Context and Correlation

  • Log Context
  • Child Loggers
  • Correlating Across Services

What to Observe

  • Business Event Logging
  • Performance Logging
  • Health and Metrics

Production

  • Log Output and Transport
  • Sensitive Data
  • Checklist and Capstone

Log Context

The missing context problem

A route handler creates a book and logs:

{ "level": "info", "message": "book created", "bookId": "book-42" }

Useful — but who created it? Which request? What endpoint? Without context, you cannot trace this log entry back to anything.

Now imagine every log entry automatically includes the request ID, user ID, and route:

{
  "level": "info",
  "message": "book created",
  "bookId": "book-42",
  "requestId": "req_a1b2c3",
  "userId": "user-7",
  "method": "POST",
  "path": "/v2/books"
}

Without adding any of those fields manually in the route handler. That is log context.

What context to attach

Always present (set in onRequest):

  • requestId — ties every log to a specific request
  • method — GET, POST, PUT, DELETE
  • path — /v2/books, /v2/books/:id

Present after authentication (set in auth logic):

  • userId — who made the request
  • role — user, admin

Present in specific handlers:

  • bookId, orderId, etc. — the resource being acted on

Storing context in locals

The Building a Logger lesson showed logger.child(). Combined with Hectoday HTTP’s locals, every handler gets a context-rich logger automatically:

export const app = setup({
  onRequest: ({ request }) => {
    const requestId = request.headers.get("x-request-id") ?? generateRequestId();
    const url = new URL(request.url);

    // Create a request-scoped logger with context already attached
    const requestLogger = logger.child({
      requestId,
      method: request.method,
      path: url.pathname,
    });

    return {
      requestId,
      startTime: Date.now(),
      method: request.method,
      path: url.pathname,
      log: requestLogger,
    };
  },
  routes: [
    /* ... */
  ],
});

The log property in locals is a child logger pre-loaded with request context. Every route handler uses it:

route.post("/v2/books", {
  request: { body: CreateBookV2 },
  resolve: (c) => {
    if (!c.input.ok) {
      c.locals.log.warn("validation failed", { issues: c.input.issues });
      return Response.json({ error: "Validation failed" }, { status: 400 });
    }

    const book = createBook(c.input.body);
    c.locals.log.info("book created", { bookId: book.id });
    // Output: {"level":"info","message":"book created","requestId":"req_a1b2c3","method":"POST","path":"/v2/books","bookId":"book-42"}

    return Response.json(book, { status: 201 });
  },
});

The handler calls c.locals.log.info(...) — it only passes the book-specific context (bookId). The request context (requestId, method, path) is already there from the child logger.

Adding user context after authentication

Authentication runs in onRequest (or early in the handler). Once you know the user, enrich the logger:

onRequest: ({ request }) => {
  const requestId = request.headers.get("x-request-id") ?? generateRequestId();
  const url = new URL(request.url);

  let userId: string | undefined;
  const authHeader = request.headers.get("authorization");
  if (authHeader) {
    try {
      const token = authHeader.replace("Bearer ", "");
      const payload = verifyToken(token);
      userId = payload.userId;
    } catch {
      // Invalid token — userId stays undefined
    }
  }

  const requestLogger = logger.child({
    requestId,
    method: request.method,
    path: url.pathname,
    ...(userId ? { userId } : {}),
  });

  return { requestId, startTime: Date.now(), method: request.method, path: url.pathname, userId, log: requestLogger };
},

Authenticated requests include userId in every log entry. Unauthenticated requests do not — no undefined fields cluttering the logs.

Exercises

Exercise 1: Create a request-scoped child logger in onRequest. Use it in a route handler. Verify the request context appears automatically.

Exercise 2: Add user ID to the logger context after authentication. Verify authenticated and unauthenticated requests produce different log shapes.

Exercise 3: Log a business event (book created) using c.locals.log. Verify it includes requestId, userId, method, path, AND bookId — without passing the first four manually.

Why use a child logger per request instead of passing context manually to each log call?

← Error Logging Child Loggers →

© 2026 hectoday. All rights reserved.