Structured Logging#
1. console.log Is Not a Logging Strategy#
console.log("something happened") tells you nothing. No timestamp. No severity. No context. No way to filter by level in production. No way to ship to a log aggregator. When an incident happens at 2am, you need to know exactly what happened and when.
Structured logging emits machine-readable JSON. Every entry has a timestamp, log level, message, and optional context fields. You can filter, search, and aggregate without grep.
Tina4 provides a Log singleton with four severity levels. Zero configuration required. Control verbosity with one environment variable.
2. The Four Log Levels#
import { Log } from "tina4-nodejs";โLog.debug("Cache lookup", { key: "product:42", hit: false });Log.info("User registered", { userId: 99, email: "alice@example.com" });Log.warn("Rate limit approaching", { ip: "203.0.113.5", requests: 95, limit: 100 });Log.error("Payment failed", { orderId: 1042, reason: "Card declined" });Each call emits a structured log line:
{"timestamp":"2026-04-02T08:12:01.234Z","level":"DEBUG","message":"Cache lookup","key":"product:42","hit":false}{"timestamp":"2026-04-02T08:12:01.235Z","level":"INFO","message":"User registered","userId":99,"email":"alice@example.com"}{"timestamp":"2026-04-02T08:12:01.236Z","level":"WARN","message":"Rate limit approaching","ip":"203.0.113.5","requests":95,"limit":100}{"timestamp":"2026-04-02T08:12:01.237Z","level":"ERROR","message":"Payment failed","orderId":1042,"reason":"Card declined"}| Level | Use for |
|---|---|
debug | Detailed diagnostic info, cache hits/misses, query plans |
info | Normal application events: logins, signups, orders placed |
warn | Unexpected but recoverable situations: retries, slow queries |
error | Failures that need attention: payment errors, crashed workers |
3. Controlling Verbosity with TINA4LOGLEVEL#
Set the minimum level in .env:
TINA4_LOG_LEVEL=infoOnly entries at or above the configured level are emitted:
TINA4_LOG_LEVEL | debug | info | warn | error |
|---|---|---|---|---|
debug | shown | shown | shown | shown |
info | silent | shown | shown | shown |
warn | silent | silent | shown | shown |
error | silent | silent | silent | shown |
In development, use debug. In production, use info or warn to reduce log volume.
# .env.developmentTINA4_LOG_LEVEL=debugโ# .env.productionTINA4_LOG_LEVEL=warn4. Adding Context Fields#
The second argument to any log method is a plain object. Its fields are merged into the log entry:
import { Log } from "tina4-nodejs";โLog.info("HTTP request", { method: "POST", path: "/api/orders", status: 201, duration_ms: 47, user_id: 15});{"timestamp":"2026-04-02T08:15:00.000Z","level":"INFO","message":"HTTP request","method":"POST","path":"/api/orders","status":201,"duration_ms":47,"user_id":15}Any JSON-serializable value is valid: strings, numbers, booleans, arrays, nested objects.
5. Logging Errors#
Pass an Error object alongside context:
import { Log } from "tina4-nodejs";โtry { await processPayment(orderId, amount);} catch (err) { Log.error("Payment processing failed", { orderId, amount, error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined });}{ "timestamp": "2026-04-02T08:16:00.000Z", "level": "ERROR", "message": "Payment processing failed", "orderId": 1042, "amount": 249.99, "error": "Connection timeout after 5000ms", "stack": "Error: Connection timeout...\n at PaymentGateway.charge ..."}6. Request-Scoped Logging#
Add a request ID to every log entry in a request handler so you can trace all log lines from a single request:
import { Router, Log } from "tina4-nodejs";import { randomUUID } from "crypto";โRouter.post("/api/checkout", async (req, res) => { const requestId = randomUUID(); const start = Date.now();โ Log.info("Checkout started", { requestId, user: req.user?.id });โ try { const orderId = await placeOrder(req.body, requestId);โ Log.info("Order placed", { requestId, orderId, duration_ms: Date.now() - start });โ return res.status(201).json({ order_id: orderId });โ } catch (err) { Log.error("Checkout failed", { requestId, error: err instanceof Error ? err.message : String(err), duration_ms: Date.now() - start });โ return res.status(500).json({ error: "Checkout failed" }); }});Search your log aggregator for requestId to see every log line from that request, in order, with timings.
7. Performance Logging#
Log slow operations to identify bottlenecks:
import { Log } from "tina4-nodejs";import { Database } from "tina4-nodejs/orm";โasync function fetchDashboardData(userId: number) { const t0 = Date.now(); const db = Database.getConnection();โ const data = await db.fetchAll( `SELECT o.id, o.total, o.status, COUNT(oi.id) as item_count FROM orders o JOIN order_items oi ON oi.order_id = o.id WHERE o.user_id = :userId GROUP BY o.id ORDER BY o.created_at DESC LIMIT 10`, { userId } );โ const duration = Date.now() - t0;โ if (duration > 500) { Log.warn("Slow dashboard query", { userId, duration_ms: duration, rows: data.length }); } else { Log.debug("Dashboard query", { userId, duration_ms: duration, rows: data.length }); }โ return data;}8. Exercise: Add Logging to an Existing API#
Take the product listing endpoint from Chapter 11 and add structured logging at every meaningful point.
Requirements#
- Log
infowhen a request is received, including the route and query parameters - Log
debugfor cache hits and misses with the cache key - Log
warnwhen a query takes longer than 200ms - Log
errorwhen an exception is caught, with the error message and stack
Expected log output (debug level):#
{"timestamp":"...","level":"INFO","message":"Products request","category":"Electronics","page":1}{"timestamp":"...","level":"DEBUG","message":"Cache miss","key":"store:products:a3f2..."}{"timestamp":"...","level":"WARN","message":"Slow product query","duration_ms":312,"rows":3}{"timestamp":"...","level":"INFO","message":"Products served","source":"database","count":3,"duration_ms":315}9. Solution#
import { Router, Log, cacheGet, cacheSet } from "tina4-nodejs";import { createHash } from "crypto";โconst PRODUCTS = [ { id: 1, name: "Wireless Keyboard", category: "Electronics", price: 79.99 }, { id: 2, name: "Yoga Mat", category: "Fitness", price: 29.99 }, { id: 3, name: "Coffee Grinder", category: "Kitchen", price: 49.99 }, { id: 4, name: "Standing Desk", category: "Electronics", price: 549.99 },];โRouter.get("/api/products/logged", async (req, res) => { const category = req.query.category ?? null; const page = parseInt(req.query.page ?? "1", 10); const start = Date.now();โ Log.info("Products request", { category, page });โ const keyData = JSON.stringify({ category, page }); const cacheKey = `products:${createHash("md5").update(keyData).digest("hex")}`;โ try { const cached = await cacheGet(cacheKey);โ if (cached !== null) { Log.debug("Cache hit", { key: cacheKey }); return res.json({ ...cached, source: "cache" }); }โ Log.debug("Cache miss", { key: cacheKey });โ // Simulate database work const t0 = Date.now(); await new Promise(resolve => setTimeout(resolve, 50)); let products = PRODUCTS;โ if (category) { products = products.filter( p => p.category.toLowerCase() === String(category).toLowerCase() ); }โ const queryDuration = Date.now() - t0;โ if (queryDuration > 200) { Log.warn("Slow product query", { duration_ms: queryDuration, rows: products.length }); }โ const result = { products, page, total: products.length }; await cacheSet(cacheKey, result, 300);โ Log.info("Products served", { source: "database", count: products.length, duration_ms: Date.now() - start });โ return res.json({ ...result, source: "database" });โ } catch (err) { Log.error("Products endpoint failed", { error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, duration_ms: Date.now() - start }); return res.status(500).json({ error: "Internal server error" }); }});10. Gotchas#
1. Logging sensitive data#
Log.info("Login", { email, password }) ships the password to your log aggregator.
Fix: Never log passwords, tokens, credit card numbers, or PII. Log user IDs and request IDs instead. Before shipping a log call to production, check every field.
2. Logging in hot paths adds latency#
Calling Log.debug() on every database row in a loop adds up.
Fix: Log aggregates, not individual items. Log.debug("Fetched rows", { count: rows.length }) is better than logging each row.
3. Circular references in context objects#
Log.info("Data", { obj }) throws if obj contains circular references, because JSON serialization fails.
Fix: Pass primitive values and simple objects. Use JSON.stringify with a replacer to handle circular references if you must log complex objects.
4. TINA4LOGLEVEL defaults to info#
In development, you might expect to see debug output and see nothing.
Fix: Set TINA4_LOG_LEVEL=debug in your .env file. Check the value with Log.debug("Log level check", {}) -- if it appears, debug logging is active.