{"post":{"slug":"logging-explained-2026","title":"Logging explained in 2026: structured logs that help you debug production","excerpt":"Good logging is the difference between fixing a production bug in 10 minutes and 10 hours. This 2026 guide covers structured logging, log levels, and observability.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">L</span>ogging is the most used debugging tool in production and the most neglected during development. When something breaks at 2 AM, your logs either tell you exactly what happened or they leave you guessing. The difference is almost entirely structure: logs as searchable, parseable JSON with consistent context fields versus free-form strings that require human pattern-matching. Getting logging right is cheap and pays continuously.\n\n## What changed in 2026\n\n- **OpenTelemetry's Logs signal graduated to stable**, meaning logs, traces, and metrics share the same context propagation. A `trace_id` that started in a trace automatically appears in logs without manual plumbing.\n- **Pino v9** (Node.js) and **structlog v24** (Python) became the clear defaults for structured logging — both emit JSON, are fast, and integrate with OTEL.\n- **Log aggregation platforms** (Datadog, Grafana Loki, AWS CloudWatch) all expect JSON natively and surface log-trace correlation automatically.\n- **`console.log` in Node.js** is now explicitly discouraged in production docs — it is synchronous, blocks the event loop on high volume, and produces plain strings.\n\n## Structured vs unstructured logging\n\n```ts\n// Unstructured — hard to search, parse, or alert on\nconsole.log(\"User 1234 placed order 5678 for $99.00\");\n\n// Structured — every field is queryable\nlogger.info({\n  userId: \"1234\",\n  orderId: \"5678\",\n  amount: 99.00,\n  event: \"order.placed\",\n});\n// Output: {\"level\":\"info\",\"userId\":\"1234\",\"orderId\":\"5678\",\"amount\":99,\"event\":\"order.placed\",\"time\":\"2026-06-03T...\"}\n```\n\nIn a structured log store, you can query `orderId = \"5678\"` or `amount > 100` across millions of lines in milliseconds.\n\n## Log levels — what each means\n\n| Level | When to use | Production default |\n| --- | --- | --- |\n| `trace` | Step-by-step execution detail | Off |\n| `debug` | Useful dev context (queries, cache hits) | Off |\n| `info` | Normal business events (request handled, order placed) | On |\n| `warn` | Unexpected but recoverable (retry, degraded mode) | On |\n| `error` | Operation failed; action may be needed | On + alert |\n| `fatal` | Process must exit | On + page |\n\nThe rule: `info` should tell the story of what the system did; `error` should wake someone up.\n\n## Setting up Pino (Node.js)\n\n```ts\n// logger.ts\nimport pino from \"pino\";\n\nexport const logger = pino({\n  level: process.env.LOG_LEVEL ?? \"info\",\n  // In production, emit JSON; in dev, use pino-pretty for human output\n  transport: process.env.NODE_ENV === \"development\"\n    ? { target: \"pino-pretty\" }\n    : undefined,\n});\n\n// Usage\nlogger.info({ userId, orderId }, \"order placed\");\nlogger.error({ err, userId }, \"payment failed\");\n```\n\n## Setting up structlog (Python)\n\n```python\n# logging_config.py\nimport structlog\nimport logging\n\nstructlog.configure(\n    processors=[\n        structlog.contextvars.merge_contextvars,\n        structlog.processors.add_log_level,\n        structlog.processors.TimeStamper(fmt=\"iso\"),\n        structlog.processors.JSONRenderer(),\n    ],\n    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),\n)\n\nlog = structlog.get_logger()\nlog.info(\"order.placed\", user_id=user_id, order_id=order_id, amount=amount)\n```\n\n## Request context propagation\n\nEvery log line in a request should carry the same trace/request ID without passing it through every function:\n\n```ts\n// Express middleware — attach request context\nimport { AsyncLocalStorage } from \"async_hooks\";\n\nconst requestContext = new AsyncLocalStorage<{ requestId: string; userId?: string }>();\n\napp.use((req, res, next) => {\n  requestContext.run({ requestId: req.headers[\"x-request-id\"] as string }, next);\n});\n\n// logger reads context automatically\nconst logger = pino({\n  mixin() {\n    return requestContext.getStore() ?? {};\n  },\n});\n```\n\nNow every `logger.info(...)` call in that request automatically includes `requestId` without threading it manually.\n\n## How to pick\n\n1. **Node.js service** — Pino. Fast async logging, JSON-native, OTEL support.\n2. **Python service** — structlog. Context vars, processor pipeline, JSON output.\n3. **Go service** — slog (stdlib since 1.21). JSON handler out of the box.\n4. **Log aggregation** — Grafana Loki (self-hosted) or Datadog Logs (managed). Both parse JSON natively.\n\n## Common mistakes\n\n**Logging inside loops or hot paths.** Logging is I/O. Even async loggers have overhead. Use `debug` level and disable it in production; never log per-row in a bulk query.\n\n**Logging PII without scrubbing.** User emails, phone numbers, and payment info in logs are a compliance liability. Redact before logging, or use a log pipeline that scrubs fields.\n\n**Giant log objects.** `logger.info({ req, res, user })` dumps thousands of lines per request. Log the IDs and specific fields, not entire objects.\n\n**Missing error objects.** `logger.error(\"payment failed\")` without `{ err }` loses the stack trace. Always pass the error as a field.\n\n## What to skip\n\n- **`console.log` in production** — no levels, synchronous, unstructured.\n- **Reinventing log transport** — use a dedicated log aggregation service rather than writing your own log shipper.\n- **Logging every function entry/exit by default** — it drowns signal in noise. Log at the boundary (HTTP handler, queue consumer) and within error paths.\n\n## FAQ\n\n**How much does logging cost in production?**\nJSON logging with Pino or structlog adds ~1–2 ms per request at moderate volume. The bigger cost is storage in your log aggregator — filter aggressively with log levels.\n\n**What is the difference between logging and tracing?**\nLogs are discrete events; traces track the flow of a request across services with timing. OpenTelemetry 2026 unifies both under a shared context so log lines link to their trace spans.\n\n**How long should I retain logs?**\n90 days for `info`/`debug` is common; 1 year for `error`/`warn` for compliance. Retention policies depend on your industry and data residency requirements.\n\n**How do I avoid logging sensitive data?**\nDefine a schema for what is allowed. Use a log processor (structlog processor or Pino `redact` option) to scrub known sensitive field names before emission.\n\n## Where to go next\n\nSee [Error handling explained in 2026](/blog/error-handling-explained-2026), [How to monitor a service in 2026](/blog/how-to-monitor-a-service-2026), and [Environment variables explained in 2026](/blog/environment-variables-explained-2026).","hue":310,"metaTitle":"Logging Explained 2026: Structured Logs That Actually Help","metaDescription":"Practical 2026 guide to logging. Structured JSON logs, log levels, request context, OpenTelemetry integration, and the tools teams use in production.","keywords":["logging explained 2026","structured logging 2026","json logs production","pino logger nodejs 2026","python structlog 2026","log levels best practices","opentelemetry logging 2026","request context logging","how to log in production","observability logging 2026"],"tldr":["**Use structured logs (JSON), not plain text strings** — machines parse JSON; grep on text does not scale past 1,000 lines.","**Set the right log level per environment** — `debug` locally, `info` in production, `warn`/`error` as triggers for alerts.","**Add request context to every log line** — trace ID, user ID, and operation name make logs searchable and traceable.","**Log at the boundary, not inside every function** — one entry log and one exit log (with duration) per request is enough.","**Skip:** `console.log` in production Node.js — it is synchronous, unstructured, and has no level filtering."],"readTime":"4 min read","wordCount":902},"related":[{"slug":"how-to-monitor-a-service-2026","title":"How to monitor a service in 2026: metrics, logs, traces, and alerts that work","excerpt":"A service without observability is a black box you only look inside after something breaks. Here is the 2026 guide to metrics, logs, traces, and alerts.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":319},{"slug":"structured-logging-guide-2026","title":"Structured Logging: Logs You Can Actually Query","excerpt":"String logs are unsearchable at scale. Here is how to move to structured events, which fields matter, and how to avoid the cost explosion that follows.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":140},{"slug":"distributed-tracing-explained-2026","title":"Distributed Tracing in 2026: How It Actually Works","excerpt":"Distributed tracing follows a single request as it moves across dozens of microservices, showing exactly where time is spent. Here is how it works and how to actually get value from it.","category":"code","date":"July 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"load-testing-guide-2026","title":"Load testing guide 2026: find your limits before your users do","excerpt":"Load testing is not about proving a system works — it is about finding where it breaks under realistic conditions before production traffic does it for you.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309},"older":{"slug":"mac-mini-vs-imac-2026","title":"Mac mini vs iMac in 2026: which desktop Mac should you buy?","excerpt":"The Mac mini and iMac share a chip but differ completely on who they are for. Here is the honest breakdown to pick the right Apple desktop in 2026.","category":"tech","date":"April 20, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":240}}