Skip to content

Commit ea7c953

Browse files
committed
Rewrite logging settings and AppLogger
1 parent 1d6085c commit ea7c953

File tree

15 files changed

+746
-201
lines changed

15 files changed

+746
-201
lines changed

plain-observer/plain/observer/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from opentelemetry.sdk.trace import TracerProvider
44
from opentelemetry.semconv.attributes import service_attributes
55

6-
from plain.logs.loggers import app_logger
6+
from plain.logs import app_logger
77
from plain.packages import PackageConfig, register_config
88
from plain.runtime import settings
99

plain-observer/plain/observer/otel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from opentelemetry.trace import SpanKind, format_span_id, format_trace_id
1111

1212
from plain.http.cookie import unsign_cookie_value
13-
from plain.logs.loggers import app_logger
13+
from plain.logs import app_logger
1414
from plain.models.otel import suppress_db_tracing
1515
from plain.runtime import settings
1616

plain-worker/plain/worker/middleware.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ def __init__(self, run_job):
66
self.run_job = run_job
77

88
def __call__(self, job):
9-
app_logger.kv.context["job_request_uuid"] = str(job.job_request_uuid)
10-
app_logger.kv.context["job_uuid"] = str(job.uuid)
11-
12-
job_result = self.run_job(job)
13-
14-
app_logger.kv.context.pop("job_request_uuid", None)
15-
app_logger.kv.context.pop("job_uuid", None)
16-
17-
return job_result
9+
with app_logger.with_context(
10+
job_request_uuid=str(job.job_request_uuid), job_uuid=str(job.uuid)
11+
):
12+
return self.run_job(job)

plain/plain/csrf/middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from urllib.parse import urlparse
44

55
from plain.exceptions import DisallowedHost
6-
from plain.logs import log_response
6+
from plain.logs.utils import log_response
77
from plain.runtime import settings
88

99
from .views import CsrfFailureView

plain/plain/internal/handlers/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from opentelemetry.semconv.attributes import http_attributes, url_attributes
66

77
from plain.exceptions import ImproperlyConfigured
8-
from plain.logs import log_response
8+
from plain.logs.utils import log_response
99
from plain.runtime import settings
1010
from plain.urls import get_resolver
1111
from plain.utils.module_loading import import_string

plain/plain/internal/handlers/exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
)
1313
from plain.http import Http404, ResponseServerError
1414
from plain.http.multipartparser import MultiPartParserError
15-
from plain.logs import log_response
15+
from plain.logs.utils import log_response
1616
from plain.runtime import settings
1717
from plain.utils.module_loading import import_string
1818
from plain.views.errors import ErrorView

plain/plain/logs/README.md

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
- [Overview](#overview)
66
- [`app_logger`](#app_logger)
7-
- [`app_logger.kv`](#app_loggerkv)
7+
- [Output formats](#output-formats)
8+
- [Context management](#context-management)
9+
- [Debug mode](#debug-mode)
10+
- [Advanced usage](#advanced-usage)
811
- [Logging settings](#logging-settings)
912

1013
## Overview
@@ -13,49 +16,124 @@ In Python, configuring logging can be surprisingly complex. For most use cases,
1316

1417
By default, both the `plain` and `app` loggers are set to the `INFO` level. You can quickly change this by using the `PLAIN_LOG_LEVEL` and `APP_LOG_LEVEL` environment variables.
1518

19+
The `app_logger` supports multiple output formats and provides a friendly kwargs-based API for structured logging.
20+
1621
## `app_logger`
1722

18-
The `app_logger` is a pre-configured logger you can use inside your app code.
23+
The `app_logger` is an enhanced logger that supports kwargs-style logging and multiple output formats.
1924

2025
```python
2126
from plain.logs import app_logger
2227

2328

2429
def example_function():
25-
app_logger.info("Hey!")
30+
# Basic logging
31+
app_logger.info("User logged in")
32+
33+
# With structured context data (explicit **context parameter)
34+
app_logger.info("User action", user_id=123, action="login", success=True)
35+
36+
# All log levels support context parameters
37+
app_logger.debug("Debug info", step="validation", count=5)
38+
app_logger.warning("Rate limit warning", user_id=456, limit_exceeded=True)
39+
app_logger.error("Database error", error_code=500, table="users")
40+
41+
# Standard logging parameters with context
42+
try:
43+
risky_operation()
44+
except Exception:
45+
app_logger.error(
46+
"Operation failed",
47+
exc_info=True, # Include exception traceback
48+
stack_info=True, # Include stack trace
49+
user_id=789,
50+
operation="risky_operation"
51+
)
2652
```
2753

28-
## `app_logger.kv`
54+
## Output formats
55+
56+
The `app_logger` supports three output formats controlled by the `APP_LOG_FORMAT` environment variable:
2957

30-
The key-value logging format is popular for outputting more structured logs that are still human-readable.
58+
### Key-Value format (default)
59+
60+
```bash
61+
export APP_LOG_FORMAT=keyvalue # or leave unset for default
62+
```
63+
64+
```
65+
[INFO] User action user_id=123 action=login success=True
66+
[ERROR] Database error error_code=500 table=users
67+
```
68+
69+
### JSON format
70+
71+
```bash
72+
export APP_LOG_FORMAT=json
73+
```
74+
75+
```json
76+
{"timestamp": "2024-01-01 12:00:00,123", "level": "INFO", "message": "User action", "user_id": 123, "action": "login", "success": true}
77+
{"timestamp": "2024-01-01 12:00:01,456", "level": "ERROR", "message": "Database error", "error_code": 500, "table": "users"}
78+
```
79+
80+
### Standard format
81+
82+
```bash
83+
export APP_LOG_FORMAT=standard
84+
```
85+
86+
```
87+
[INFO] User action
88+
[ERROR] Database error
89+
```
90+
91+
Note: In standard format, the context kwargs are ignored and not displayed.
92+
93+
## Context management
94+
95+
The `app_logger` provides powerful context management for adding data to multiple log statements.
96+
97+
### Persistent context
98+
99+
Use the `context` dict to add data that persists across log calls:
31100

32101
```python
33-
from plain.logs import app_logger
102+
# Set persistent context
103+
app_logger.context["user_id"] = 123
104+
app_logger.context["request_id"] = "abc456"
34105

106+
app_logger.info("Started processing") # Includes user_id and request_id
107+
app_logger.info("Validation complete") # Includes user_id and request_id
108+
app_logger.info("Processing finished") # Includes user_id and request_id
35109

36-
def example_function():
37-
app_logger.kv("Example log line with", example_key="example_value")
110+
# Clear context
111+
app_logger.context.clear()
38112
```
39113

40-
## Logging settings
114+
### Temporary context
41115

42-
You can further configure your logging with `settings.LOGGING`.
116+
Use `include_context()` for temporary context that only applies within a block:
43117

44118
```python
45-
# app/settings.py
46-
LOGGING = {
47-
"version": 1,
48-
"disable_existing_loggers": False,
49-
"handlers": {
50-
"console": {
51-
"class": "logging.StreamHandler",
52-
},
53-
},
54-
"loggers": {
55-
"mylogger": {
56-
"handlers": ["console"],
57-
"level": "DEBUG",
58-
},
59-
},
60-
}
119+
app_logger.context["user_id"] = 123 # Persistent
120+
121+
with app_logger.include_context(operation="payment", transaction_id="txn789"):
122+
app_logger.info("Payment started") # Has user_id, operation, transaction_id
123+
app_logger.info("Payment validated") # Has user_id, operation, transaction_id
124+
125+
app_logger.info("Payment complete") # Only has user_id
126+
```
127+
128+
## Debug mode
129+
130+
The `force_debug()` context manager allows temporarily enabling DEBUG level logging:
131+
132+
```python
133+
# Debug messages might not show at INFO level
134+
app_logger.debug("This might not appear")
135+
136+
# Temporarily enable debug logging
137+
with app_logger.force_debug():
138+
app_logger.debug("This will definitely appear", extra_data="debug_info")
61139
```

plain/plain/logs/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from .configure import configure_logging
21
from .loggers import app_logger
3-
from .utils import log_response
42

5-
__all__ = ["app_logger", "log_response", "configure_logging"]
3+
__all__ = ["app_logger"]

plain/plain/logs/configure.py

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
import logging
2-
import logging.config
3-
from os import environ
4-
5-
6-
def configure_logging(logging_settings):
7-
# Load the defaults
8-
default_logging = {
9-
"version": 1,
10-
"disable_existing_loggers": False,
11-
"formatters": {
12-
"simple": {
13-
"format": "[%(levelname)s] %(message)s",
14-
},
15-
},
16-
"handlers": {
17-
"plain_console": {
18-
"level": "DEBUG",
19-
"class": "logging.StreamHandler",
20-
"formatter": "simple",
21-
},
22-
"app_console": {
23-
"level": "DEBUG",
24-
"class": "logging.StreamHandler",
25-
"formatter": "simple",
26-
},
27-
},
28-
"loggers": {
29-
"plain": {
30-
"handlers": ["plain_console"],
31-
"level": environ.get("PLAIN_LOG_LEVEL", "INFO"),
32-
},
33-
"app": {
34-
"handlers": ["app_console"],
35-
"level": environ.get("APP_LOG_LEVEL", "INFO"),
36-
"propagate": False,
37-
},
38-
},
39-
}
40-
logging.config.dictConfig(default_logging)
41-
42-
# Then customize it from settings
43-
if logging_settings:
44-
logging.config.dictConfig(logging_settings)
2+
3+
from .formatters import JSONFormatter, KeyValueFormatter
4+
5+
6+
def configure_logging(*, plain_log_level, app_log_level, app_log_format):
7+
# Create and configure the plain logger (uses standard Logger, not AppLogger)
8+
plain_logger = logging.Logger("plain")
9+
plain_logger.setLevel(plain_log_level)
10+
plain_handler = logging.StreamHandler()
11+
plain_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
12+
plain_logger.addHandler(plain_handler)
13+
plain_logger.propagate = False
14+
logging.root.manager.loggerDict["plain"] = plain_logger
15+
16+
# Configure the existing app_logger
17+
from .loggers import app_logger
18+
19+
app_logger.setLevel(app_log_level)
20+
app_logger.propagate = False
21+
22+
app_handler = logging.StreamHandler()
23+
match app_log_format:
24+
case "json":
25+
app_handler.setFormatter(JSONFormatter("%(json)s"))
26+
case "keyvalue":
27+
app_handler.setFormatter(
28+
KeyValueFormatter("[%(levelname)s] %(message)s %(keyvalue)s")
29+
)
30+
case _:
31+
app_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
32+
33+
app_logger.addHandler(app_handler)
34+
35+
# Register the app_logger in the logging system so getLogger("app") returns it
36+
logging.root.manager.loggerDict["app"] = app_logger

plain/plain/logs/debug.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import logging
2+
import threading
3+
4+
5+
class DebugMode:
6+
"""Context manager to temporarily set DEBUG level on a logger with reference counting."""
7+
8+
def __init__(self, logger):
9+
self.logger = logger
10+
self.original_level = None
11+
self._ref_count = 0
12+
self._lock = threading.Lock()
13+
14+
def __enter__(self):
15+
"""Store original level and set to DEBUG."""
16+
self.start()
17+
return self
18+
19+
def __exit__(self, exc_type, exc_val, exc_tb):
20+
"""Restore original level."""
21+
self.end()
22+
23+
def start(self):
24+
"""Enable DEBUG logging level."""
25+
with self._lock:
26+
if self._ref_count == 0:
27+
self.original_level = self.logger.level
28+
self.logger.setLevel(logging.DEBUG)
29+
self._ref_count += 1
30+
31+
def end(self):
32+
"""Restore original logging level."""
33+
with self._lock:
34+
self._ref_count = max(0, self._ref_count - 1)
35+
if self._ref_count == 0:
36+
self.logger.setLevel(self.original_level)

0 commit comments

Comments
 (0)