---
title: "Introduction"
description: "What a wide event is, and why one per operation beats a line per step."
---

> Documentation Index
> Fetch the complete documentation index at: https://widelog-py.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Introduction

widelog emits one structured event per operation instead of a line per step. Every field you
attach during a request lands in the same JSON object, alongside how long the operation took and
any error that ended it. It has no dependencies outside the standard library.

## The shape of the problem

A single request usually leaves a trail like this:

```python
print("Request received")
print(f"User: {user.id}")
print("Cart loaded")
print("Payment failed")
raise Exception("Something went wrong")
```

Five lines, interleaved with every other concurrent request, and none of them knows about the
others. To answer "what else was true when this failed?" you have to reconstruct the request by
correlating timestamps.

## One event instead

This is the whole program. Click Run and it executes in your browser, on real CPython compiled to
WebAssembly, with the same widelog source the package ships.

```python
from widelog import ErrorCatalog, ErrorSpec, init, use_logger, wide_event

init(service="checkout", environment="local")

class BillingErrors(ErrorCatalog, prefix="billing"):
INSUFFICIENT_FUNDS = ErrorSpec(
    status=402,
    message=lambda available, required: f"Insufficient funds: {available} of {required}",
    why="The account balance did not cover the cart total",
    fix="Add funds and retry",
)

def load_cart(user_id):
return {"items": 3, "total": 9999}

def charge(card, token):
# Nobody passed this function a logger. It writes to the same event anyway.
use_logger().set(payment={"method": "card", "last4": card[-4:], "token": token})
raise BillingErrors.INSUFFICIENT_FUNDS(available=5000, required=9999)

with wide_event(method="POST", path="/api/checkout") as log:
log.set(user={"id": "u_123", "plan": "premium"})
log.set(cart=load_cart("u_123"))
try:
    charge("4242424242424242", "idem_abc")
except Exception as exc:
    log.error(exc)
```

One event comes out, carrying everything the request collected:

```json
{
  "timestamp": "2026-07-30T04:22:51.213Z",
  "level": "error",
  "service": "checkout",
  "environment": "local",
  "duration_ms": 0.1,
  "method": "POST",
  "path": "/api/checkout",
  "user": { "id": "u_123", "plan": "premium" },
  "cart": { "items": 3, "total": 9999 },
  "payment": { "method": "card", "last4": "4242", "token": "[REDACTED]" },
  "error": {
"message": "Insufficient funds: 5000 of 9999",
"status": 402,
"code": "billing.INSUFFICIENT_FUNDS",
"why": "The account balance did not cover the cart total",
"fix": "Add funds and retry",
"type": "WidelogError",
"stack": ["main.py:29 in <module>", "main.py:22 in charge"]
  }
}
```

Four things happened there without being asked for. `charge()` never received a logger and still
wrote to the event. The `token` came out redacted. The error carried its own status, code, `why`,
and `fix`, because the catalog declared them once. And the level became `error` on its own.

Querying this is filtering rather than correlating: `status = 402 and user.plan = "premium"` is one
condition over one table.

## What you get

- **Ambient context** — `use_logger()` reads a `contextvars` slot, so a helper five frames deep adds fields without
being handed a logger.
- **Errors that explain themselves** — `why` says what went wrong, `fix` says what to do next. Declare them once in a catalog.
- **One adapter per runtime** — ASGI covers FastAPI, Starlette, Litestar, and Django-async. A decorator covers AWS Lambda.
- **Secrets stay out** — Key names ending in `token`, `password`, `secret`, and friends are redacted at any depth.

Source: https://widelog-py.pages.dev/introduction/index.mdx
