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

Project Setup

The starting point

The book catalog API from previous courses — books, authors, reviews. Currently using console.log everywhere. By the end of this course, every log will be structured JSON with context.

Create the project

mkdir observable-catalog
cd observable-catalog
npm init -y
npm install @hectoday/http zod srvx better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3 tsx

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "rootDir": "./src",
    "outDir": "dist",
    "types": ["node"]
  },
  "include": ["src"]
}

Add "type": "module" and a dev script to package.json:

{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts"
  }
}

To start the server:

npm run dev

The current state (console.log everywhere)

// src/app.ts — BEFORE (unstructured logging)
import { setup, route } from "@hectoday/http";
import db from "./db.js";

export const app = setup({
  onRequest: ({ request }) => {
    console.log("Request:", request.method, new URL(request.url).pathname);
    return {};
  },
  onError: ({ error }) => {
    console.error("Error:", error.message);
    return Response.json({ error: "Internal error" }, { status: 500 });
  },
  routes: [
    route.get("/books", {
      resolve: () => {
        console.log("Fetching books...");
        const books = db.prepare("SELECT * FROM books").all();
        console.log("Found", books.length, "books");
        return Response.json(books);
      },
    }),
  ],
});

The output:

Request: GET /books
Fetching books...
Found 5 books
Request: POST /books
Error: UNIQUE constraint failed
Request: GET /books/nonexistent

No timestamps, no levels, no request IDs, no searchable structure. This is what we are replacing.

The database (same as previous courses)

// src/db.ts
import Database from "better-sqlite3";

const db = new Database("catalog.db");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");

db.exec(`
  CREATE TABLE IF NOT EXISTS authors (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    bio TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
  );
  CREATE TABLE IF NOT EXISTS books (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    author_id TEXT NOT NULL,
    genre TEXT NOT NULL,
    description TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY (author_id) REFERENCES authors(id)
  );
  CREATE TABLE IF NOT EXISTS reviews (
    id TEXT PRIMARY KEY,
    book_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
    body TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY (book_id) REFERENCES books(id)
  );
`);

export default db;

The goal

By the end of this course, the same request produces:

{
  "timestamp": "2024-01-15T14:32:05.123Z",
  "level": "info",
  "message": "request completed",
  "requestId": "req_a1b2c3",
  "method": "GET",
  "path": "/books",
  "status": 200,
  "duration": 12
}

Structured. Timestamped. Searchable. Every log entry tied to a request with a unique ID.

Exercises

Exercise 1: Start the server. Make 10 requests. Look at the console output. Try to find a specific request.

Exercise 2: Count every console.log and console.error in the project. These will all be replaced.

Exercise 3: Write down what information you wish each log entry had (timestamp, user, request ID, etc.).

What is the first problem to solve when replacing console.log?

← What to Log JSON Logs →

© 2026 hectoday. All rights reserved.