Is Loguru Good for Production Python Services?
I was setting up logging for a new Python service and thought I’d finally try Loguru. Everyone says it’s “just better” than the standard logging module. But when I mentioned it to a colleague, their first question was: “Is Loguru good for production? What about OpenTelemetry?”
That question sent me down a rabbit hole. Here’s what I found.
The Short Answer
Yes, Loguru is production-ready — but with one caveat: if you need OpenTelemetry integration for distributed tracing, you’ll have extra work compared to alternatives like structlog.
+-------------------+ +------------------+ +------------------+| Your Use Case | | Observability? | | Recommendation |+-------------------+ +------------------+ +------------------+| CLI Tool / Script | --> | No | --> | Loguru (Easy!) |+-------------------+ +------------------+ +------------------+| Simple Service | --> | No | --> | Loguru (Easy!) |+-------------------+ +------------------+ +------------------+| Microservice | --> | Yes | --> | structlog (Easier|| | | | | OTel integration)|+-------------------+ +------------------+ +------------------+A Reddit comment summed it up perfectly: “Loguru is the easiest to set up but needs an extra indirection layer for OpenTelemetry.”
What Makes Loguru Shine in Production
I started with the simplest possible setup:
from loguru import logger
# That's literally it. No handlers, no formatters, no config.logger.info("Application started")logger.error("Something failed: {}", error_message)Compare that to Python’s standard logging module, where you need to configure handlers, formatters, log levels, and still get ugly output. Loguru just works.
But for production, I needed more. Here’s what I discovered:
Built-in Features That Actually Matter
import sysfrom loguru import logger
# Remove the default handler firstlogger.remove()
# Console output for container logslogger.add( sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", level="INFO")
# File rotation - this is where Loguru shineslogger.add( "logs/app_{time}.log", rotation="500 MB", # Rotate at 500MB retention="10 days", # Auto-delete old logs compression="zip", # Compress rotated files level="DEBUG")
# JSON output for log aggregation (ELK, etc.)logger.add( "logs/json_{time}.log", serialize=True, # Automatic JSON formatting rotation="1 day")The key production features I get out of the box:
- File rotation: No more
logrotatecron jobs - Retention policies: Old logs actually get cleaned up
- Compression: Rotated logs are zipped automatically
- Thread-safe: No race conditions in multi-threaded apps
- Exception context: Tracebacks include variable values
The OpenTelemetry Problem
Here’s where things got interesting. I was building a microservice that needed distributed tracing, and I wanted to correlate logs with traces.
With structlog, you add a processor:
import structlogfrom opentelemetry import trace
def add_otel_context(_, __, event_dict): span = trace.get_current_span() if span.is_recording(): ctx = span.get_span_context() event_dict["trace_id"] = format(ctx.trace_id, "032x") event_dict["span_id"] = format(ctx.span_id, "016x") return event_dict
structlog.configure( processors=[ structlog.stdlib.add_log_level, structlog.processors.TimeStamper(fmt="iso"), add_otel_context, # <-- One line! structlog.processors.JSONRenderer() ])With Loguru? It’s possible, but requires a patcher function:
import sysfrom loguru import loggerfrom opentelemetry.trace import ( INVALID_SPAN, INVALID_SPAN_CONTEXT, get_current_span, get_tracer_provider,)
def instrument_loguru(): """Add OpenTelemetry trace context to Loguru logs.""" provider = get_tracer_provider() service_name = None
def add_trace_context(record): # Initialize defaults record["extra"]["otelSpanID"] = "0" record["extra"]["otelTraceID"] = "0" record["extra"]["otelTraceSampled"] = False
# Get service name once nonlocal service_name if service_name is None: resource = getattr(provider, "resource", None) if resource: service_name = resource.attributes.get("service.name") or "" record["extra"]["otelServiceName"] = service_name
# Extract trace context span = get_current_span() if span != INVALID_SPAN: ctx = span.get_span_context() if ctx != INVALID_SPAN_CONTEXT: record["extra"]["otelSpanID"] = format(ctx.span_id, "016x") record["extra"]["otelTraceID"] = format(ctx.trace_id, "032x") record["extra"]["otelTraceSampled"] = ctx.trace_flags.sampled
logger.configure(patcher=add_trace_context)
# Apply instrumentationinstrument_loguru()
# Configure format to include trace IDsformat_ = ( "{time:YYYY-MM-DD HH:mm:ss.SSS} {level} [{name}] " "[trace_id={extra[otelTraceID]} span_id={extra[otelSpanID]}] " "- {message}")logger.add(sys.stderr, format=format_)That’s significantly more code. Does it work? Yes. Is it elegant? Not really.
A Reddit user put it well: “For anything going to production I’d probably go structlog based on your benchmarks alone.”
Performance and Security Considerations
I ran into two more things worth mentioning:
Performance
According to Reddit benchmarks, structlog is approximately 2x faster than Loguru. For most services, this won’t matter. But if you’re logging millions of events per second, keep it in mind.
Performance Comparison (approximate):+------------+------------+------------------+| Library | Throughput | Notes |+------------+------------+------------------+| structlog | ~2x faster | Optimized for || | | structured logs |+------------+------------+------------------+| Loguru | Baseline | Still fast enough|| | | for most uses |+------------+------------+------------------+Security: The Diagnose Flag
This one bit me. Loguru’s diagnose=True (the default) includes variable values in exception tracebacks. In development, this is incredibly useful. In production, it’s a security risk.
from loguru import logger
# CRITICAL for productionlogger.add("production.log", diagnose=False)
# diagnose=False prevents logging:# - Passwords# - API keys# - User PII# - Internal stateI learned this the hard way when a traceback exposed an API key in our logs. Always set diagnose=False in production.
When to Choose What
After all this experimentation, here’s my decision matrix:
Choose Loguru When:
- Building CLI tools or scripts
- Setting up a simple service without distributed tracing
- You want beautiful default output
- You value “install and done” over “configure everything”
- The team is new to Python logging
from loguru import loggerimport sys
def setup_logging(): """Production setup for a simple service.""" logger.remove() logger.add( sys.stderr, format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | " "<level>{level: <8}</level> | " "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | " "<level>{message}</level>", level="INFO" ) logger.add( "logs/service.log", rotation="00:00", # Daily rotation retention="30 days", compression="gz", level="DEBUG", diagnose=False # Security first ) logger.add( "logs/json.log", serialize=True, # For log aggregation rotation="500 MB", level="INFO" )Choose structlog When:
- Building microservices with tracing requirements
- You need native OpenTelemetry integration
- Performance is critical
- Using frameworks like FastAPI with existing OTel middleware
- Your observability stack relies on distributed tracing
My Final Take
I still use Loguru for most projects. The simplicity is hard to beat, and the “just works” experience saves me hours of configuration.
But for services where distributed tracing matters — where I need to follow a request across multiple services through logs — structlog’s native OpenTelemetry support makes my life easier.
The question isn’t “Is Loguru production-ready?” It absolutely is. The real question is: Do you need distributed tracing?
If no, Loguru all the way. If yes, consider the extra OTel integration effort, or go with structlog.
As one Reddit comment noted: “Both require quite a bit of massaging to make them work as intended.” Neither is perfect, but both are far better than raw Python logging.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments