Standardized structured (JSON) logging package with built-in Sentry integration for Python applications.
- Structured JSON logging - Outputs to stdout for log aggregation systems (ELK, Datadog, etc.)
- Automatic Sentry integration - Error tracking and performance monitoring with zero configuration
- Automatic trace correlation - Logs across your entire call stack share the same
trace_id - FastAPI automatic request tracing - Each HTTP request gets a unique trace ID
- Clean schema - Standard fields at top level, custom fields nested in
details
- Python >= 3.8
- FastAPI (required for automatic request tracing)
uv add sentry-struct-logger
# With Lambda support (for AWS Lambda functions)
uv add "python-sentry-logger-wrapper[lambda] @ git+https://github.com/HEAL-Engineering/python-sentry-logger-wrapper.git"
# Or using pip
pip install sentry-struct-loggerfrom fastapi import FastAPI
from python_sentry_logger_wrapper import get_logger
import os
# Initialize logger with Sentry BEFORE creating FastAPI app
logger = get_logger(
service_name="api-service",
sentry_dsn=os.getenv("SENTRY_DSN"),
sentry_environment="production"
)
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
logger.info("Fetching user from API")
user = await fetch_user(user_id)
return user
async def fetch_user(user_id: int):
logger.info("Querying database", user_id=user_id)
return await db.query(...)What happens automatically:
- Each HTTP request gets a unique
trace_id - ALL logs during that request share the same
trace_id - Logs appear in stdout as JSON AND in Sentry with full request context
- Errors are automatically linked to the request that caused them
# API Layer
from python_sentry_logger_wrapper import get_logger
logger = get_logger("example-api", sentry_dsn=os.getenv("SENTRY_DSN"))
@app.post("/items")
async def create_item(item: Item):
logger.info("Creating item", item_count=len(item.data))
result = await item_service.create(item)
logger.info("Item created", item_id=result.id)
return result
# Service Layer
logger = get_logger("example-api")
async def create(item: Item):
logger.info("Validating item")
await validate_data(item.data)
logger.info("Processing external request")
response = await external_service.process(item)
if not response.success:
logger.error("External service failed", reason=response.error_code)
raise ServiceError(response.error_code)
logger.info("Saving to database")
return await db.items.create(item)Console output (all logs share the same trace_id):
{"timestamp": "...", "log_level": "INFO", "logger": "example-api", "message": "Creating item", "trace_id": "abc123...", "span_id": "def456...", "details": {"item_count": 3}}
{"timestamp": "...", "log_level": "INFO", "logger": "example-api", "message": "Validating item", "trace_id": "abc123...", "span_id": "def456..."}
{"timestamp": "...", "log_level": "INFO", "logger": "example-api", "message": "Processing external request", "trace_id": "abc123...", "span_id": "def456..."}
{"timestamp": "...", "log_level": "INFO", "logger": "example-api", "message": "Saving to database", "trace_id": "abc123...", "span_id": "def456..."}In Sentry:
- Click the trace to see all logs in timeline
- View request duration, endpoint, and status code
- If an error occurs, see which request caused it with full context
timestamp- ISO 8601 UTC timestamplog_level- INFO, WARNING, ERROR, etc.logger- Your service identifier (the name passed toget_logger())message- Log messagetrace_id- Distributed tracing ID (only present when Sentry is enabled)span_id- Span ID for the current operation (only present when Sentry is enabled)
Any additional fields you pass are automatically nested:
logger.info("User login", user_id=123, ip="10.0.1.100", method="oauth")
# Output: {..., "details": {"user_id": 123, "ip": "10.0.1.100", "method": "oauth"}}- Create a project at sentry.io
- Copy your DSN from Settings → Client Keys
- Set it as an environment variable:
export SENTRY_DSN="https://..."
Sentry offers 5,000 errors/events per month free - perfect for small projects.
logger = get_logger(
service_name="my-service",
log_level=logging.INFO, # Minimum log level for stdout
sentry_dsn="https://...", # Optional: enables Sentry
sentry_environment="production", # Optional: environment tag
sentry_breadcrumbs_level=logging.INFO, # Min level for Sentry breadcrumbs
sentry_event_level=logging.ERROR, # Min level captured as Sentry events (Issues)
sentry_logs_level=logging.INFO, # Min level captured by Sentry Logs product
traces_sample_rate=0.0, # Performance tracing sample rate (0.0 = off)
renderer="auto", # "json" (default), "console", or "auto"
)Two parameters let you dial back Sentry usage without forking the package — useful for projects on the free plan or chatty services that would otherwise burn through quota.
| Parameter | Default | What it controls | When to change |
|---|---|---|---|
sentry_logs_level |
logging.INFO |
Minimum level captured by Sentry's Logs product (separate from Errors/Issues). | Raise to WARNING or ERROR to stop info-level chatter from filling Sentry Logs without affecting error capture. |
traces_sample_rate |
0.0 |
Performance tracing sample rate (0.0–1.0). Passed to sentry_sdk.init. |
Leave at 0.0 unless you need tracing. Set to 0.1 (10%) for sampled tracing. Above that on a busy service eats the free plan's 10k performance units/month quickly. |
The three log-level knobs are intentionally distinct and map 1:1 to LoggingIntegration:
| Parameter | Maps to | Default | Purpose |
|---|---|---|---|
sentry_breadcrumbs_level |
LoggingIntegration(level=...) |
INFO |
Context attached to errors |
sentry_event_level |
LoggingIntegration(event_level=...) |
ERROR |
Becomes an Issue in the Errors product |
sentry_logs_level |
LoggingIntegration(sentry_logs_level=...) |
INFO |
Structured logs in the Logs product |
get_logger() accepts renderer="json" | "console" | "auto" to control how logs are formatted on stdout. The default is "json" — existing consumers see zero behavior change.
| Mode | Output | When to use |
|---|---|---|
"json" (default) |
Structured JSON, one event per line | Production. Anywhere logs feed an aggregator (ELK, Datadog, CloudWatch, Loki, etc.). |
"console" |
Colored, human-readable via structlog.dev.ConsoleRenderer |
Local development. Reading logs in your terminal. |
"auto" |
"console" when sys.stdout.isatty() is True, else "json" |
Set-and-forget. Mirrors git / ls --color=auto. |
sys.stdout.isatty() returns:
Truewhen stdout is an interactive terminal → renders as console.Falsewhen stdout is redirected to a file, piped to another process, or running under Docker / CI / Kubernetes / systemd → renders as JSON.
That means a consumer can set renderer="auto" once and get colored logs while developing, and JSON the moment their service ships — no environment branching required.
Sentry's LoggingIntegration intercepts events at the stdlib logging.Handler level — before the renderer runs. Switching between "json" and "console" only changes what hits stdout; Sentry ingestion (errors, breadcrumbs, trace context) is identical either way.
# Production service feeding a log aggregator — default works fine
logger = get_logger(service_name="my-service")
# Local dev script — pretty colored output
logger = get_logger(service_name="my-service", renderer="console")
# Best of both: pretty in your shell, JSON when piped to a file or shipped to a container
logger = get_logger(service_name="my-service", renderer="auto")If you currently call get_logger(...) without the renderer argument, the default stays "json" — your output won't change. Opt into "console" or "auto" only where you want it.
- ERROR/CRITICAL logs - Sent as searchable events
- INFO/WARNING logs - Sent as breadcrumbs (attached to errors for context)
- All custom fields - Searchable in Sentry UI (e.g.,
details.user_id:123) - Request context - Automatic with FastAPI (URL, method, headers, duration)
For AWS Lambda functions, enable the Lambda integration for automatic timeout warnings and Lambda-specific context:
from typing import Optional
from pydantic_settings import BaseSettings
from python_sentry_logger_wrapper import get_logger
class Settings(BaseSettings):
"""Lambda configuration using Pydantic BaseSettings."""
service_name: str = "my-lambda"
sentry_dsn: Optional[str] = None
environment: str = "unknown"
class Config:
env_prefix = "" # or use a prefix like "LAMBDA_"
settings = Settings()
logger = get_logger(
service_name=settings.service_name,
sentry_dsn=settings.sentry_dsn,
sentry_environment=settings.environment,
lambda_integration=True, # Enables AwsLambdaIntegration
lambda_timeout_warning=True, # Warn before Lambda timeout (default: True)
)
def lambda_handler(event, context):
logger.info("Processing event", event_type=event.get("triggerSource"))
# Your Lambda logic here
return {"statusCode": 200}What happens automatically:
- Lambda context (function name, memory, request ID) added to logs
- Timeout warnings before Lambda times out
- Errors captured with full Lambda context
try:
result = await process_data(item)
except ProcessingError as e:
logger.error(
"Data processing failed",
error_type=type(e).__name__,
item_id=item.id,
exc_info=True # Includes full stack trace
)
raiselogger.debug("Detailed debugging info", query="SELECT * FROM users")
logger.info("Normal operation", status="healthy")
logger.warning("Degraded performance", latency_ms=2500)
logger.error("Operation failed", retry_count=3)
logger.critical("System down", reason="database_unavailable")Q: Do I need to pass trace_id manually through my functions?
A: No! It's automatically propagated through your entire call stack via Sentry's tracing.
Q: Can I use this without Sentry?
A: Yes! Just omit sentry_dsn and you'll get JSON logs to stdout only.
Q: Does Sentry integration affect my JSON stdout logs? A: No, logs are sent to both Sentry AND stdout independently. Your log aggregation system still works.
Q: How do I search logs in Sentry?
A: Custom fields are under details, so search like: details.user_id:123 or details.transaction_id:txn_*
Q: Can I use this without FastAPI? A: Yes, but automatic request tracing requires FastAPI. Without it, you'll need to manage trace context manually.
MIT