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 whenGREL_ENV_LOADis enabled, or passed straight toconfigure().
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:
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