---
title: "Logging estructurado con un formato de log y niveles que puedes buscar por campo"
description: "El logging estructurado pone cada valor en su propio campo en vez de dentro de una frase. Enviamos 1 pedido de 3 formas a LogBrew y mostramos cuál puedes volver a encontrar por order_id."
canonical: "https://logbrew.co/es/guides/structured-logging"
last_updated: "2026-11-10"
---

# Logging estructurado con un formato de log y niveles que puedes buscar por campo

El logging estructurado pone cada valor en su propio campo en vez de dentro de una frase. Enviamos 1 pedido de 3 formas a LogBrew y mostramos cuál puedes volver a encontrar por `order_id`.

Probado el 10 de noviembre de 2026 con Python 3.13.5 y logbrew-sdk 0.1.29

Por [Furkan Erday](https://github.com/furkanerday). Furkan Erday es el fundador de LogBrew. Desarrolla LogBrew con Claude Code y Codex todos los días.

Canonical: https://logbrew.co/es/guides/structured-logging

## Qué línea de log puedes volver a encontrar por `order_id`

| Dónde fue el id | Búsqueda de texto | Filtro por campo |
| --- | --- | --- |
| Dentro del texto del mensaje | Encontrado | No encontrado |
| En campos `extra={}` | No encontrado | Encontrado |
| En un dict dentro de `extra={}` | No encontrado | No encontrado |

Enviamos 1 pedido de 3 formas a través del handler de Python y luego lo buscamos. Una taza llena significa que la búsqueda encontró la línea.

## Elige un formato de log que puedas filtrar

### Deja el mensaje fijo y pon los valores en campos

Un campo conserva su nombre y su tipo. Un valor dentro del texto del mensaje es solo una subcadena. En Python, pasa los campos en `extra={}` y el mensaje queda igual para cada pedido.

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### Escribe líneas JSON cuando un programa lee el log

Una subclase de `Formatter` convierte cada registro en 1 objeto JSON por línea, con los campos estándar y cada valor de `extra`. Un colector de logs analiza la línea sin regex.

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

Salida

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### Envía los campos a LogBrew

`LogBrewLoggingHandler` envía cada registro con sus valores de `extra` como campos. Enviamos el mismo pedido de 3 formas, dentro del texto del mensaje, como campos `extra` y como un dict dentro de `extra`.

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

Salida

```text
HTTP 200 accepted 3 logs
```

### Vuelve a encontrar la línea por campo

`GET /api/logs` acepta `attribute_filters` con una ruta de campo, un tipo, un operador y un valor. El filtro por `order_id` encontró solo la línea enviada con `extra`. El dict dentro de `extra` se descartó, porque el handler solo conserva cadenas, números, booleanos y `None`.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Salida

```text
{"message":"order paid","order_id":"ord_42"}
```

## Usa los niveles de log igual en todos los servicios

### Usa 4 niveles en producción

`info` para lo que pasó, `warning` para lo que salió mal pero se gestionó, `error` para una solicitud o un trabajo que falló y `critical` para lo que detiene el servicio. Deja `debug` para ejecuciones locales. El handler de LogBrew envía las llamadas DEBUG como `info`.

### Fija el nivel una vez, al arrancar

Lee el nivel del entorno, así 1 host puede correr en DEBUG mientras el resto se queda en INFO. En Python lo hace `logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))`.

### Registra los números como números

`amount_cents: 1999` conserva su tipo, así que el filtro `amount_cents >= 1000` lo encuentra. También enviamos `amount_cents: "1999"` como texto, y el filtro numérico saltó esa línea.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

Salida

```text
{"message":"order paid","amount_cents":1999}
```

## Lo que vimos al probarlo

Enviamos 1 pedido de 3 formas a través del handler de Python y luego lo buscamos con la API de LogBrew. La búsqueda de texto `search=ord_42` encontró 1 de las 3 líneas, la que tenía el id dentro del mensaje.

El filtro `order_id = ord_42` encontró 1 línea, la enviada con `extra={"order_id": "ord_42"}`. El filtro `amount_cents >= 1000` encontró la misma línea, porque el handler conservó 1999 como número.

La línea enviada con `extra={"order": {...}}` no coincidió con nada. El handler envía cadenas, números, booleanos y `None` desde `extra` y omite un dict o una lista.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Salida

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## Texto plano vs JSON como formato de log

Un formato `%(message)s` plano descarta cada valor de `extra`. La línea de abajo termina en `INFO order paid` sin id de pedido, así que nadie puede encontrar el pedido después.

Una línea JSON conserva los campos y sus tipos, y un colector de logs la analiza sin regex. Una persona la lee con menos facilidad, así que mantén el mensaje corto y los nombres de campo estables.

Python no tiene un formateador JSON integrado. python/cpython#77691 pidió uno en 2018 y se cerró, porque la documentación ya muestra cómo escribirlo. El `Formatter` de 15 líneas de arriba es esa receta.

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

Salida

```text
2026-11-10 08:34:04,955 INFO order paid
```

## Lo que LogBrew registra

Cada log conserva su nivel, el nombre del logger y cada campo primitivo de `extra`, más los campos de `metadata={}` en el handler. La API filtra por ruta de campo, tipo y operador. El texto acepta `eq`, `ne`, `contains` y `starts_with`, y los números aceptan `gt`, `gte`, `lt` y `lte`.

LogBrew no indexa un campo cuyo nombre contiene `password`, `secret`, `token`, `cookie` o `authorization`, así que un filtro nunca lo encuentra. Aun así, deja los secretos fuera de las líneas de log.

Un log enviado dentro de una traza lleva los ids de traza y de span, así que un filtro por `order_id` lleva a la solicitud que produjo la línea.

## Preguntas frecuentes

### ¿Qué es el logging estructurado?

Logging donde cada valor es un campo con nombre y tipo, como JSON o como campos de un registro de log, en vez de palabras dentro de una frase. Una herramienta de búsqueda filtra entonces por el campo, no por subcadena.

### ¿Cuáles son los niveles de log en orden?

DEBUG, INFO, WARNING, ERROR, CRITICAL, con los números 10 a 50 en Python. Un logger fijado en INFO deja pasar INFO y superiores y descarta DEBUG.

### ¿Registro en JSON o en texto plano?

JSON cuando un programa lee las líneas, como un colector, un archivo o una herramienta de búsqueda. Texto plano cuando solo una persona lee la terminal. Mantén los mismos campos en ambos.

### ¿Cómo añado campos a una línea de log de Python?

Pasa `extra={"order_id": "ord_42"}` a la llamada de log. El formateador estándar ignora los campos extra. Un formateador JSON o el handler de LogBrew los conserva.

### ¿Puedo buscar logs por campo en LogBrew?

Sí. `GET /api/logs` acepta hasta 4 `attribute_filters`, cada uno con una ruta de campo, un tipo y un operador como `eq` o `gte`.

## Filtra tus logs por cualquier campo

LogBrew conserva cada campo que registras y vuelve a encontrar la línea por campo, junto a la traza y la incidencia a las que pertenece. Empieza con el generoso plan gratuito, sin tarjeta.

[Empezar gratis](https://logbrew.co/es)

## Fuentes

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## Más guías para agentes y Python

- [Añade un servidor MCP a Claude Code y Codex](https://logbrew.co/es/guides/mcp-server-claude-code-codex)
- [Usa AGENTS.md en Claude Code y Codex](https://logbrew.co/es/guides/agents-md)
- [Soluciona el error 529 overloaded y el error 429 de rate limit en Claude](https://logbrew.co/es/guides/claude-overloaded-rate-limit-error)
- [Configura el logging de Python con los niveles de log correctos y captura de excepciones](https://logbrew.co/es/guides/python-logging)
- [Cómo solucionar errores en Claude Code, Codex, JavaScript y Python](https://logbrew.co/es/guides)
