Skip to content

Logging

Zero-config logging that follows the 12-factor app methodology. Use it to get structured, environment-aware logs without wiring handlers by hand.

  • Zero-config: logs go to stdout, the format is picked automatically.
  • Structured: extra fields become flat top-level keys, exceptions become structured error data.
  • Environment-driven: every knob is a GREL_LOG_* environment variable, read when GREL_ENV_LOAD is enabled, or passed straight to configure().

Two more pages cover the rest: Integrations for OpenTelemetry, FastAPI, and uvicorn, and Filters for taming a noisy logger.

Quick Start

from grelmicro.log import configure

configure()

Or attach it to a Grelmicro app via uses=:

import asyncio
import logging

from grelmicro import Grelmicro
from grelmicro.log import Log

micro = Grelmicro(uses=[Log()])


async def main() -> None:
    async with micro:
        logging.getLogger(__name__).info("hello", extra={"user_id": 123})


asyncio.run(main())

Log() accepts the same knobs as configure() and resolves GREL_LOG_* environment variables. On exit, the previous stdlib root handlers are restored.

With no environment variables set, configure() detects your terminal:

  • Terminal (TTY): human-readable colored text
  • Piped / CI / container: structured JSON

This is the AUTO format (the default). Most users never need to set GREL_LOG_FORMAT.

Backends

grelmicro supports three logging backends. All backends produce identical output for each format, so switching is easy. Select one with the GREL_LOG_BACKEND environment variable, then use the matching logger.

Backend Dependencies Best for
stdlib (default) None Zero-dependency setups
Loguru loguru Developer ergonomics
structlog structlog High-throughput services
import logging

configure()
logger = logging.getLogger(__name__)
logger.info("Hello, World!", extra={"user_id": 123})
from loguru import logger

configure()
logger.info("Hello, World!", user_id=123)
import structlog

configure()
log = structlog.get_logger()
log.info("Hello, World!", user_id=123)
Installation

No additional dependencies required. Uses Python's built-in logging module.

pip install grelmicro[standard]
pip install grelmicro[structlog]
pip install grelmicro[standard,opentelemetry]
# or
pip install grelmicro[structlog,opentelemetry]

Every level and an exception, on the loguru backend:

basic.py
from loguru import logger

from grelmicro.log import configure

configure()

logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message with context", user="Alice")
logger.error("This is an error message with context", user="Bob")

try:
    raise ValueError("This is an exception message")  # noqa: EM101, TRY003, TRY301
except ValueError:
    logger.exception(
        "This is an exception message with context", user="Charlie"
    )

Structured Logging

Extra context fields are passed as keyword arguments and appear as flat top-level fields:

"""Example: Structured logging with context."""

from loguru import logger

from grelmicro.log import configure

# Ensure clean state
logger.remove()

configure()

logger.info("User logged in", user_id=123, ip_address="192.168.1.1")

Output:

{"time":"...","level":"INFO","msg":"User logged in","logger":"...","user_id":123,"ip_address":"192.168.1.1"}

Exception Handling

Exceptions are automatically captured as structured ErrorDict:

"""Example: Exception logging with context."""

from loguru import logger

from grelmicro.log import configure

# Ensure clean state
logger.remove()

configure()

try:
    1 / 0  # noqa: B018
except ZeroDivisionError:
    logger.exception("Operation failed", operation="divide")

JSON output:

{"time":"...","level":"ERROR","msg":"Operation failed","logger":"...","operation":"divide","error":{"type":"ZeroDivisionError","message":"division by zero","stack":"..."}}

LOGFMT and PRETTY output

LOGFMT output:

time=... level=ERROR msg="Operation failed" logger=... error.type=ZeroDivisionError error.message="division by zero" error.stack="Traceback..."

PRETTY output:

  ... ERROR Operation failed
    at ...
    operation: divide
    error.type: ZeroDivisionError
    error.message: division by zero
    error.stack:
      Traceback (most recent call last):
        ...
      ZeroDivisionError: division by zero

Log Formats

grelmicro provides five format options, following common structured-logging conventions:

Format Use Case Machine-Parseable
AUTO Default. Adapts to environment Depends
JSON Production, log aggregation Yes
LOGFMT Structured + human-readable Yes
TEXT Local development No
PRETTY Verbose debugging No

AUTO (Default)

Detects the output target and selects the best format automatically:

Condition Selected Format
stdout is a TTY (terminal) TEXT (colored)
stdout is piped or redirected JSON
FORCE_COLOR env var set TEXT (colored)
NO_COLOR env var set JSON
"""Example: AUTO format logging (default)."""

from loguru import logger

from grelmicro.log import configure

logger.remove()

# AUTO is the default: TEXT in terminal, JSON when piped.
# No LOG_FORMAT env var needed.
configure()

logger.info("Application started", version="1.0.0")

In your terminal:

2026-04-01 10:30:00.123 INFO     __main__ - Application started version=1.0.0

In a container or CI:

{"time":"2026-04-01T08:30:00.123456+00:00","level":"INFO","msg":"Application started","logger":"__main__","version":"1.0.0"}

JSON, LOGFMT, TEXT, and PRETTY formats

JSON

Structured newline-delimited JSON. Ideal for production, log aggregation (Datadog, Loki, ELK).

GREL_LOG_FORMAT=JSON
"""Example: JSON format logging with timezone."""

from loguru import logger

from grelmicro.log import configure

# Ensure clean state
logger.remove()

configure()

logger.info("Application started", version="1.0.0", environment="production")

Output:

{"time":"2026-04-01T10:30:00.123456+02:00","level":"INFO","msg":"Application started","logger":"__main__","version":"1.0.0","environment":"production"}

LOGFMT

Key-value pairs following the logfmt convention. 30-40% smaller than JSON, grep-friendly, parseable by Grafana Loki and most log tools.

GREL_LOG_FORMAT=LOGFMT
"""Example: LOGFMT format logging."""

from loguru import logger

from grelmicro.log import configure

logger.remove()

configure()

logger.info("Request handled", method="GET", path="/health", status=200)

Output:

time=2026-04-01T10:30:00.123456+00:00 level=INFO msg="Request handled" logger=__main__ method=GET path=/health status=200

Nested dicts use dot notation:

error.type=ValueError error.message="invalid input"

TEXT

Single-line, human-readable output. Includes extra fields as key=value pairs. Colors are enabled when output is a TTY.

GREL_LOG_FORMAT=TEXT
"""Example: TEXT format logging with timezone."""

from loguru import logger

from grelmicro.log import configure

# Ensure clean state
logger.remove()

configure()

logger.info("Application started", version="1.0.0")

Output:

2026-04-01 10:30:00.123 INFO     __main__:<module>:12 - Application started version=1.0.0

PRETTY

Multi-line format with indented fields. Best for debugging with low log volume.

GREL_LOG_FORMAT=PRETTY
"""Example: PRETTY format logging."""

from loguru import logger

from grelmicro.log import configure

logger.remove()

configure()

logger.info("Request handled", method="GET", path="/health", status=200)

Output:

  2026-04-01 10:30:00.123 INFO Request handled
    at __main__:<module>:10
    method: GET
    path: /health
    status: 200

With exceptions:

  2026-04-01 10:30:01.456 ERROR Operation failed
    at myapp.service:process:78
    error.type: ZeroDivisionError
    error.message: division by zero
    error.stack:
      Traceback (most recent call last):
        File "service.py", line 78, in process
          result = 1 / 0
      ZeroDivisionError: division by zero

Custom Format (Loguru only)

You can provide a custom loguru format template:

GREL_LOG_FORMAT="{level} | {message}"
"""Example: Custom format logging."""

from loguru import logger

from grelmicro.log import configure

# Ensure clean state
logger.remove()

configure()

logger.info("Custom format example")

Output:

INFO | Custom format example

Note

Custom format strings only work with the loguru backend.

Settings

Every setting can be passed to configure() directly, or read from the environment:

Environment variables are opt-in

A GREL_* variable that fills a component field is read only when GREL_ENV_LOAD is truthy (1, true, yes, on), the flag itself and GREL_ENVIRONMENT excepted. Without it the variable is ignored and the default applies. Setting one while the flag is unset warns at startup, so the mistake is not silent. Passing env_load=False is a deliberate opt-out and stays quiet. See How a value is resolved for the three ways to configure a component, including a local .env.

Variable Values Default
GREL_LOG_BACKEND stdlib, loguru, structlog stdlib
GREL_LOG_LEVEL DEBUG, INFO, WARNING, ERROR, CRITICAL INFO
GREL_LOG_FORMAT AUTO, JSON, LOGFMT, TEXT, PRETTY AUTO
GREL_LOG_TIMEZONE IANA timezone (e.g., UTC, Europe/Zurich) UTC
GREL_LOG_JSON_SERIALIZER stdlib, orjson stdlib
GREL_LOG_CALLER_ENABLED true, false false
GREL_LOG_OTEL_ENABLED true, false auto-detected
GREL_LOG_UVICORN_ENABLED true, false true
NO_COLOR any value (unset)
FORCE_COLOR any value (unset)

Color Support

Colors follow the NO_COLOR and FORCE_COLOR standards. When NO_COLOR is set, AUTO resolves to JSON and colors are disabled. FORCE_COLOR takes precedence over NO_COLOR.

Timezone

The GREL_LOG_TIMEZONE setting controls timestamps in all formats:

GREL_LOG_TIMEZONE=Europe/Zurich

JSON / LOGFMT: ISO 8601 with timezone offset

"time":"2026-04-01T15:56:36.066922+02:00"

TEXT / PRETTY: local time with its offset, Z when that is UTC

2026-04-01 15:56:36.066+02:00
2026-04-01 13:56:36.066Z

Leave it unset to follow GREL_TIMEZONE, the wall clock the whole service runs on. Set it to UTC to keep log timestamps on UTC under a service that schedules on local time.

Why orjson is not selected automatically

Installing orjson does not change anything until you also select it. That is deliberate, and it is the one place in the logging module that is not auto-detected.

The two serializers do not agree on every payload:

Value in extra={...} stdlib orjson
float("nan"), float("inf") NaN, Infinity null
a non-string dict key coerced to a string raises TypeError

Picking a serializer because a package happens to be importable would mean an unrelated dependency pulling in orjson could change what your logs say, or turn a working log call into an exception on a payload that used to serialize. A log line that crashes the request is worse than a log line that is slower.

So the choice stays yours. Set GREL_LOG_JSON_SERIALIZER=orjson, or pass json_serializer="orjson" to configure(), once you know your payloads are compatible. See the benchmarks for what it buys.

JSON Record Structure

All JSON log records follow this schema. Required fields are always present, optional fields may be absent. Extra context fields are merged flat at the top level:

class JSONRecordDict:
    # Required
    time: str              # ISO 8601 timestamp with timezone
    level: str             # DEBUG, INFO, WARNING, ERROR, CRITICAL
    msg: str               # Log message
    logger: str            # Logger name (e.g., "myapp.api")
    # Optional (opt-in via GREL_LOG_CALLER_ENABLED=true)
    caller: str            # function:line (e.g., "handle:45")
    # Optional
    trace_id: str          # OpenTelemetry trace ID (32 hex chars)
    span_id: str           # OpenTelemetry span ID (16 hex chars)
    error: ErrorDict       # Structured error info

The ErrorDict structure:

class ErrorDict:
    type: str              # Exception class name (e.g., "ValueError")
    message: str           # Exception message
    stack: str             # Optional: full traceback string
Design decisions

Level casing: UPPERCASE (DEBUG, INFO, WARNING, ERROR, CRITICAL), following common structured-logging conventions.

Field naming: Core field names (time, level, msg, logger, caller, error) follow common structured-logging conventions. logger is the logger name, caller is the call site (function:line).

Caller opt-in: caller is disabled by default, as in many structured-logging libraries. Enable with GREL_LOG_CALLER_ENABLED=true. Uvicorn formatters never include caller (points to uvicorn internals, not application code).

Collision protection: Core fields cannot be overwritten by user-supplied extra context.

Production Deployment

For strict unbuffered output (12-factor compliance):

PYTHONUNBUFFERED=1