Skip to content

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.

Decision Matrix
+-------------------+ +------------------+ +------------------+
| 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:

basic_setup.py
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

production_config.py
import sys
from loguru import logger
# Remove the default handler first
logger.remove()
# Console output for container logs
logger.add(
sys.stderr,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
level="INFO"
)
# File rotation - this is where Loguru shines
logger.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 logrotate cron 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:

structlog_otel.py
import structlog
from 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:

loguru_otel.py
import sys
from loguru import logger
from 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 instrumentation
instrument_loguru()
# Configure format to include trace IDs
format_ = (
"{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
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.

security_config.py
from loguru import logger
# CRITICAL for production
logger.add("production.log", diagnose=False)
# diagnose=False prevents logging:
# - Passwords
# - API keys
# - User PII
# - Internal state

I 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
loguru_simple_service.py
from loguru import logger
import 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