How to Do Structured JSON Line Logging in Python
Create a simple JSON-lines logger that writes one JSON object per line to stdout with timestamp, level, message, and custom context fields.
Python code
23 linesimport json
import sys
from datetime import datetime
class JsonLineLogger:
def __init__(self, stream=sys.stdout):
self.stream = stream
def log(self, level, message, **context):
record = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": level,
"message": message,
**context
}
self.stream.write(json.dumps(record) + "\n")
self.stream.flush()
if __name__ == "__main__":
logger = JsonLineLogger()
logger.log("INFO", "Application started", app="demo", version="1.0.0")
logger.log("WARNING", "High memory usage", memory_mb=1024, threshold_mb=512)
logger.log("ERROR", "Failed to connect", host="localhost", port=8080, retries=3)
Output
{"timestamp": "2025-01-01T00:00:00.000000Z", "level": "INFO", "message": "Application started", "app": "demo", "version": "1.0.0"}
{"timestamp": "2025-01-01T00:00:00.000000Z", "level": "WARNING", "message": "High memory usage", "memory_mb": 1024, "threshold_mb": 512}
{"timestamp": "2025-01-01T00:00:00.000000Z", "level": "ERROR", "message": "Failed to connect", "host": "localhost", "port": 8080, "retries": 3}
How it works
This code builds a dictionary per log call, then serializes it with json.dumps and writes it to the stream followed by a newline. Using json.dumps ensures all context values are converted to valid JSON types (e.g., integers stay numbers, not strings). The flush() call makes each line immediately visible, which matters for real-time tailing or piping to a log collector. The datetime.utcnow().isoformat() plus "Z" gives a UTC timestamp in ISO 8601 format, a common standard for machine logs. By accepting **context, the logger accepts any number of extra fields, making it flexible for production use.
Common mistakes
- Using `print()` with dict formatting instead of `json.dumps`, which may produce invalid JSON for nested objects.
- Forgetting to add a newline, causing multiple records to merge into one line and break line-based log parsers.
- Using `datetime.now()` instead of `datetime.utcnow()` for logs, leading to timezone-inconsistent timestamps.
- Not flushing the stream, so lines may be buffered and lost on a crash.
Variations
- Use `datetime.now(timezone.utc).isoformat()` in Python 3.9+ for a timezone-aware timestamp.
- Replace the custom class with a lambda or function that formats and writes directly to a file handler.
Real-world use cases
- Sending application logs to a log aggregator like ELK or Loki that expects JSON-lines format.
- Storing audit records in a file where each line is a searchable JSON event for compliance.
- Streaming user actions or errors to a message queue for real-time monitoring and alerting.
Sponsored
More from Observability & SRE
- Adding a Correlation ID to Log Context in Python medium
- Calculate Error Rate from Log Stream in Python easy
- Check if a Timestamp Falls in a Daily Maintenance Window in Python easy
- Export Metrics with OTLP Mock in Python medium
- Generate Mock CPU and Memory Metrics in Python easy
- Generate Prometheus Text Exposition Format in Python easy
Keep learning
Related tutorials and quizzes for this topic.