plain 0.68.0__py3-none-any.whl → 0.103.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +234 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/logs/README.md
CHANGED
|
@@ -1,139 +1,238 @@
|
|
|
1
1
|
# Logs
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Structured logging with sensible defaults and zero configuration.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
|
-
- [
|
|
6
|
+
- [Using app_logger](#using-app_logger)
|
|
7
|
+
- [Basic logging](#basic-logging)
|
|
8
|
+
- [Adding context](#adding-context)
|
|
7
9
|
- [Output formats](#output-formats)
|
|
10
|
+
- [Key-value format](#key-value-format)
|
|
11
|
+
- [JSON format](#json-format)
|
|
12
|
+
- [Standard format](#standard-format)
|
|
8
13
|
- [Context management](#context-management)
|
|
14
|
+
- [Persistent context](#persistent-context)
|
|
15
|
+
- [Temporary context](#temporary-context)
|
|
9
16
|
- [Debug mode](#debug-mode)
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
17
|
+
- [Output streams](#output-streams)
|
|
18
|
+
- [Settings reference](#settings-reference)
|
|
19
|
+
- [FAQs](#faqs)
|
|
20
|
+
- [Installation](#installation)
|
|
12
21
|
|
|
13
22
|
## Overview
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
Python's logging module is powerful but notoriously difficult to configure. Plain provides a ready-to-use logging setup that works out of the box while supporting structured logging for production environments.
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
You get two pre-configured loggers: `plain` (for framework internals) and `app` (for your application code). Both default to the `INFO` level and can be adjusted via environment variables.
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
```python
|
|
29
|
+
from plain.logs import app_logger
|
|
30
|
+
|
|
31
|
+
# Simple message logging
|
|
32
|
+
app_logger.info("Application started")
|
|
33
|
+
|
|
34
|
+
# Structured logging with context data
|
|
35
|
+
app_logger.info("User logged in", context={"user_id": 123, "method": "oauth"})
|
|
36
|
+
|
|
37
|
+
# All log levels work the same way
|
|
38
|
+
app_logger.warning("Rate limit approaching", context={"requests": 95, "limit": 100})
|
|
39
|
+
app_logger.error("Payment failed", context={"order_id": "abc-123", "reason": "insufficient_funds"})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Using app_logger
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
### Basic logging
|
|
22
45
|
|
|
23
|
-
The `app_logger`
|
|
46
|
+
The [`app_logger`](./app.py#AppLogger) supports all standard logging levels: `debug`, `info`, `warning`, `error`, and `critical`.
|
|
24
47
|
|
|
25
48
|
```python
|
|
26
|
-
|
|
49
|
+
app_logger.debug("Entering validation step")
|
|
50
|
+
app_logger.info("Request processed successfully")
|
|
51
|
+
app_logger.warning("Cache miss, falling back to database")
|
|
52
|
+
app_logger.error("Failed to connect to external service")
|
|
53
|
+
app_logger.critical("Database connection pool exhausted")
|
|
54
|
+
```
|
|
27
55
|
|
|
56
|
+
### Adding context
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
# Basic logging
|
|
31
|
-
app_logger.info("User logged in")
|
|
58
|
+
Pass structured data using the `context` parameter. This data appears in your log output based on your chosen format.
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
```python
|
|
61
|
+
app_logger.info("Order placed", context={
|
|
62
|
+
"order_id": "ord-456",
|
|
63
|
+
"items": 3,
|
|
64
|
+
"total": 99.99,
|
|
65
|
+
})
|
|
66
|
+
```
|
|
35
67
|
|
|
36
|
-
|
|
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")
|
|
68
|
+
You can also include exception tracebacks:
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
operation="risky_operation"
|
|
51
|
-
)
|
|
70
|
+
```python
|
|
71
|
+
try:
|
|
72
|
+
process_payment()
|
|
73
|
+
except PaymentError:
|
|
74
|
+
app_logger.error(
|
|
75
|
+
"Payment processing failed",
|
|
76
|
+
exc_info=True,
|
|
77
|
+
context={"order_id": "ord-456"},
|
|
78
|
+
)
|
|
52
79
|
```
|
|
53
80
|
|
|
54
81
|
## Output formats
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
Control the log format with the `APP_LOG_FORMAT` environment variable.
|
|
84
|
+
|
|
85
|
+
### Key-value format
|
|
57
86
|
|
|
58
|
-
|
|
87
|
+
The default format. Context data appears as `key=value` pairs, easy for humans to read and machines to parse.
|
|
59
88
|
|
|
60
89
|
```bash
|
|
61
|
-
export APP_LOG_FORMAT=keyvalue
|
|
90
|
+
export APP_LOG_FORMAT=keyvalue
|
|
62
91
|
```
|
|
63
92
|
|
|
64
93
|
```
|
|
65
|
-
[INFO] User
|
|
66
|
-
[ERROR]
|
|
94
|
+
[INFO] User logged in user_id=123 method=oauth
|
|
95
|
+
[ERROR] Payment failed order_id="abc-123" reason="insufficient_funds"
|
|
67
96
|
```
|
|
68
97
|
|
|
69
98
|
### JSON format
|
|
70
99
|
|
|
100
|
+
Each log entry is a single JSON object. Ideal for log aggregation services like Datadog, Splunk, or CloudWatch.
|
|
101
|
+
|
|
71
102
|
```bash
|
|
72
103
|
export APP_LOG_FORMAT=json
|
|
73
104
|
```
|
|
74
105
|
|
|
75
106
|
```json
|
|
76
|
-
{"timestamp": "2024-01-
|
|
77
|
-
{"timestamp": "2024-01-01 12:00:01,456", "level": "ERROR", "message": "Database error", "error_code": 500, "table": "users"}
|
|
107
|
+
{"timestamp": "2024-01-15 10:30:00,123", "level": "INFO", "message": "User logged in", "logger": "app", "user_id": 123, "method": "oauth"}
|
|
78
108
|
```
|
|
79
109
|
|
|
80
110
|
### Standard format
|
|
81
111
|
|
|
112
|
+
A minimal format that omits context data entirely.
|
|
113
|
+
|
|
82
114
|
```bash
|
|
83
115
|
export APP_LOG_FORMAT=standard
|
|
84
116
|
```
|
|
85
117
|
|
|
86
118
|
```
|
|
87
|
-
[INFO] User
|
|
88
|
-
[ERROR] Database error
|
|
119
|
+
[INFO] User logged in
|
|
89
120
|
```
|
|
90
121
|
|
|
91
|
-
Note: In standard format, the context kwargs are ignored and not displayed.
|
|
92
|
-
|
|
93
122
|
## Context management
|
|
94
123
|
|
|
95
|
-
The `app_logger` provides powerful context management for adding data to multiple log statements.
|
|
96
|
-
|
|
97
124
|
### Persistent context
|
|
98
125
|
|
|
99
|
-
|
|
126
|
+
Add context that applies to all subsequent log calls by modifying the `context` dict directly.
|
|
100
127
|
|
|
101
128
|
```python
|
|
102
|
-
# Set
|
|
103
|
-
app_logger.context["
|
|
104
|
-
app_logger.context["
|
|
129
|
+
# Set context at the start of a request
|
|
130
|
+
app_logger.context["request_id"] = "req-789"
|
|
131
|
+
app_logger.context["user_id"] = 42
|
|
105
132
|
|
|
106
|
-
app_logger.info("
|
|
107
|
-
app_logger.info("
|
|
108
|
-
app_logger.info("Processing finished") # Includes user_id and request_id
|
|
133
|
+
app_logger.info("Starting request") # Includes request_id and user_id
|
|
134
|
+
app_logger.info("Fetching data") # Includes request_id and user_id
|
|
109
135
|
|
|
110
|
-
# Clear
|
|
136
|
+
# Clear when done
|
|
111
137
|
app_logger.context.clear()
|
|
112
138
|
```
|
|
113
139
|
|
|
114
140
|
### Temporary context
|
|
115
141
|
|
|
116
|
-
Use `include_context()`
|
|
142
|
+
Use `include_context()` when you need context for a specific block of code.
|
|
117
143
|
|
|
118
144
|
```python
|
|
119
|
-
app_logger.context["user_id"] =
|
|
145
|
+
app_logger.context["user_id"] = 42
|
|
120
146
|
|
|
121
|
-
with app_logger.include_context(operation="
|
|
122
|
-
app_logger.info("
|
|
123
|
-
app_logger.info("
|
|
147
|
+
with app_logger.include_context(operation="checkout", cart_id="cart-123"):
|
|
148
|
+
app_logger.info("Starting checkout") # Has user_id, operation, cart_id
|
|
149
|
+
app_logger.info("Validating items") # Has user_id, operation, cart_id
|
|
124
150
|
|
|
125
|
-
app_logger.info("
|
|
151
|
+
app_logger.info("Checkout complete") # Only has user_id
|
|
126
152
|
```
|
|
127
153
|
|
|
128
154
|
## Debug mode
|
|
129
155
|
|
|
130
|
-
|
|
156
|
+
When you need to temporarily see debug-level logs (even if the logger is set to `INFO`), use `force_debug()`.
|
|
131
157
|
|
|
132
158
|
```python
|
|
133
|
-
#
|
|
134
|
-
app_logger.debug("
|
|
159
|
+
# These won't show if log level is INFO
|
|
160
|
+
app_logger.debug("Detailed trace info")
|
|
135
161
|
|
|
136
|
-
# Temporarily enable debug
|
|
162
|
+
# Temporarily enable debug output
|
|
137
163
|
with app_logger.force_debug():
|
|
138
|
-
app_logger.debug("This will
|
|
164
|
+
app_logger.debug("This will appear")
|
|
165
|
+
app_logger.debug("So will this", context={"step": "validation"})
|
|
166
|
+
|
|
167
|
+
# Back to normal
|
|
168
|
+
app_logger.debug("This won't show again")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The [`DebugMode`](./debug.py#DebugMode) class handles reference counting, so nested `force_debug()` calls work correctly.
|
|
172
|
+
|
|
173
|
+
## Output streams
|
|
174
|
+
|
|
175
|
+
By default, Plain splits log output by severity:
|
|
176
|
+
|
|
177
|
+
- **DEBUG, INFO** go to `stdout`
|
|
178
|
+
- **WARNING, ERROR, CRITICAL** go to `stderr`
|
|
179
|
+
|
|
180
|
+
This helps cloud platforms automatically classify log severity. You can change this behavior with `PLAIN_LOG_STREAM`:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Default: split by level
|
|
184
|
+
export PLAIN_LOG_STREAM=split
|
|
185
|
+
|
|
186
|
+
# All logs to stdout
|
|
187
|
+
export PLAIN_LOG_STREAM=stdout
|
|
188
|
+
|
|
189
|
+
# All logs to stderr (traditional Python behavior)
|
|
190
|
+
export PLAIN_LOG_STREAM=stderr
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Settings reference
|
|
194
|
+
|
|
195
|
+
All logging settings use environment variables:
|
|
196
|
+
|
|
197
|
+
| Environment Variable | Default | Description |
|
|
198
|
+
| --------------------------- | ---------- | ------------------------------------------------ |
|
|
199
|
+
| `PLAIN_FRAMEWORK_LOG_LEVEL` | `INFO` | Log level for the `plain` logger |
|
|
200
|
+
| `PLAIN_LOG_LEVEL` | `INFO` | Log level for the `app` logger |
|
|
201
|
+
| `PLAIN_LOG_FORMAT` | `keyvalue` | Output format: `json`, `keyvalue`, or `standard` |
|
|
202
|
+
| `PLAIN_LOG_STREAM` | `split` | Output stream: `split`, `stdout`, or `stderr` |
|
|
203
|
+
|
|
204
|
+
Valid log levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
|
205
|
+
|
|
206
|
+
## FAQs
|
|
207
|
+
|
|
208
|
+
#### How do I use a custom logger instead of app_logger?
|
|
209
|
+
|
|
210
|
+
You can use Python's standard `logging.getLogger()` for additional loggers. They won't have the context features of `app_logger`, but they'll use Plain's output configuration.
|
|
211
|
+
|
|
212
|
+
#### Can I use app_logger in library code?
|
|
213
|
+
|
|
214
|
+
The `app_logger` is designed for application code. If you're writing a reusable library, use `logging.getLogger(__name__)` to allow users to configure logging independently.
|
|
215
|
+
|
|
216
|
+
#### Why are my debug logs not showing?
|
|
217
|
+
|
|
218
|
+
The default log level is `INFO`. Set `PLAIN_LOG_LEVEL=DEBUG` in your environment or use `app_logger.force_debug()` temporarily.
|
|
219
|
+
|
|
220
|
+
#### How do I add context to exception logs?
|
|
221
|
+
|
|
222
|
+
Pass both `exc_info=True` and `context` to include both the traceback and structured data:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
except Exception:
|
|
226
|
+
app_logger.error("Operation failed", exc_info=True, context={"operation": "sync"})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Installation
|
|
230
|
+
|
|
231
|
+
`plain.logs` is included with Plain by default. No additional installation is required.
|
|
232
|
+
|
|
233
|
+
To adjust log levels for development, add environment variables to your shell or `.env` file:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
PLAIN_LOG_LEVEL=DEBUG
|
|
237
|
+
PLAIN_FRAMEWORK_LOG_LEVEL=WARNING
|
|
139
238
|
```
|
plain/logs/__init__.py
CHANGED
plain/logs/{loggers.py → app.py}
RENAMED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
4
|
+
from collections.abc import Generator
|
|
2
5
|
from contextlib import contextmanager
|
|
6
|
+
from typing import Any
|
|
3
7
|
|
|
4
8
|
from .debug import DebugMode
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class AppLogger(logging.Logger):
|
|
8
|
-
"""Enhanced logger that supports
|
|
12
|
+
"""Enhanced logger that supports context dict logging and context management."""
|
|
9
13
|
|
|
10
|
-
def __init__(self, name):
|
|
14
|
+
def __init__(self, name: str):
|
|
11
15
|
super().__init__(name)
|
|
12
|
-
self.context = {} # Public, mutable context dict
|
|
16
|
+
self.context: dict[str, Any] = {} # Public, mutable context dict
|
|
13
17
|
self.debug_mode = DebugMode(self)
|
|
14
18
|
|
|
15
19
|
@contextmanager
|
|
16
|
-
def include_context(self, **kwargs):
|
|
20
|
+
def include_context(self, **kwargs: Any) -> Generator[None, None, None]:
|
|
17
21
|
"""Context manager for temporary context."""
|
|
18
22
|
# Store original context
|
|
19
23
|
original_context = self.context.copy()
|
|
@@ -27,21 +31,21 @@ class AppLogger(logging.Logger):
|
|
|
27
31
|
# Restore original context
|
|
28
32
|
self.context = original_context
|
|
29
33
|
|
|
30
|
-
def force_debug(self):
|
|
34
|
+
def force_debug(self) -> DebugMode:
|
|
31
35
|
"""Return context manager for temporarily enabling DEBUG level logging."""
|
|
32
36
|
return self.debug_mode
|
|
33
37
|
|
|
34
38
|
# Override logging methods with explicit parameters for IDE support
|
|
35
|
-
def debug(
|
|
39
|
+
def debug( # type: ignore[override]
|
|
36
40
|
self,
|
|
37
|
-
msg,
|
|
38
|
-
*args,
|
|
39
|
-
exc_info=None,
|
|
40
|
-
extra=None,
|
|
41
|
-
stack_info=False,
|
|
42
|
-
stacklevel=1,
|
|
43
|
-
|
|
44
|
-
):
|
|
41
|
+
msg: object,
|
|
42
|
+
*args: object,
|
|
43
|
+
exc_info: Any = None,
|
|
44
|
+
extra: dict[str, Any] | None = None,
|
|
45
|
+
stack_info: bool = False,
|
|
46
|
+
stacklevel: int = 1,
|
|
47
|
+
context: dict[str, Any] | None = None,
|
|
48
|
+
) -> None:
|
|
45
49
|
if self.isEnabledFor(logging.DEBUG):
|
|
46
50
|
self._log(
|
|
47
51
|
logging.DEBUG,
|
|
@@ -51,19 +55,19 @@ class AppLogger(logging.Logger):
|
|
|
51
55
|
extra=extra,
|
|
52
56
|
stack_info=stack_info,
|
|
53
57
|
stacklevel=stacklevel,
|
|
54
|
-
|
|
58
|
+
context=context,
|
|
55
59
|
)
|
|
56
60
|
|
|
57
|
-
def info(
|
|
61
|
+
def info( # type: ignore[override]
|
|
58
62
|
self,
|
|
59
|
-
msg,
|
|
60
|
-
*args,
|
|
61
|
-
exc_info=None,
|
|
62
|
-
extra=None,
|
|
63
|
-
stack_info=False,
|
|
64
|
-
stacklevel=1,
|
|
65
|
-
|
|
66
|
-
):
|
|
63
|
+
msg: object,
|
|
64
|
+
*args: object,
|
|
65
|
+
exc_info: Any = None,
|
|
66
|
+
extra: dict[str, Any] | None = None,
|
|
67
|
+
stack_info: bool = False,
|
|
68
|
+
stacklevel: int = 1,
|
|
69
|
+
context: dict[str, Any] | None = None,
|
|
70
|
+
) -> None:
|
|
67
71
|
if self.isEnabledFor(logging.INFO):
|
|
68
72
|
self._log(
|
|
69
73
|
logging.INFO,
|
|
@@ -73,19 +77,19 @@ class AppLogger(logging.Logger):
|
|
|
73
77
|
extra=extra,
|
|
74
78
|
stack_info=stack_info,
|
|
75
79
|
stacklevel=stacklevel,
|
|
76
|
-
|
|
80
|
+
context=context,
|
|
77
81
|
)
|
|
78
82
|
|
|
79
|
-
def warning(
|
|
83
|
+
def warning( # type: ignore[override]
|
|
80
84
|
self,
|
|
81
|
-
msg,
|
|
82
|
-
*args,
|
|
83
|
-
exc_info=None,
|
|
84
|
-
extra=None,
|
|
85
|
-
stack_info=False,
|
|
86
|
-
stacklevel=1,
|
|
87
|
-
|
|
88
|
-
):
|
|
85
|
+
msg: object,
|
|
86
|
+
*args: object,
|
|
87
|
+
exc_info: Any = None,
|
|
88
|
+
extra: dict[str, Any] | None = None,
|
|
89
|
+
stack_info: bool = False,
|
|
90
|
+
stacklevel: int = 1,
|
|
91
|
+
context: dict[str, Any] | None = None,
|
|
92
|
+
) -> None:
|
|
89
93
|
if self.isEnabledFor(logging.WARNING):
|
|
90
94
|
self._log(
|
|
91
95
|
logging.WARNING,
|
|
@@ -95,19 +99,19 @@ class AppLogger(logging.Logger):
|
|
|
95
99
|
extra=extra,
|
|
96
100
|
stack_info=stack_info,
|
|
97
101
|
stacklevel=stacklevel,
|
|
98
|
-
|
|
102
|
+
context=context,
|
|
99
103
|
)
|
|
100
104
|
|
|
101
|
-
def error(
|
|
105
|
+
def error( # type: ignore[override]
|
|
102
106
|
self,
|
|
103
|
-
msg,
|
|
104
|
-
*args,
|
|
105
|
-
exc_info=None,
|
|
106
|
-
extra=None,
|
|
107
|
-
stack_info=False,
|
|
108
|
-
stacklevel=1,
|
|
109
|
-
|
|
110
|
-
):
|
|
107
|
+
msg: object,
|
|
108
|
+
*args: object,
|
|
109
|
+
exc_info: Any = None,
|
|
110
|
+
extra: dict[str, Any] | None = None,
|
|
111
|
+
stack_info: bool = False,
|
|
112
|
+
stacklevel: int = 1,
|
|
113
|
+
context: dict[str, Any] | None = None,
|
|
114
|
+
) -> None:
|
|
111
115
|
if self.isEnabledFor(logging.ERROR):
|
|
112
116
|
self._log(
|
|
113
117
|
logging.ERROR,
|
|
@@ -117,19 +121,19 @@ class AppLogger(logging.Logger):
|
|
|
117
121
|
extra=extra,
|
|
118
122
|
stack_info=stack_info,
|
|
119
123
|
stacklevel=stacklevel,
|
|
120
|
-
|
|
124
|
+
context=context,
|
|
121
125
|
)
|
|
122
126
|
|
|
123
|
-
def critical(
|
|
127
|
+
def critical( # type: ignore[override]
|
|
124
128
|
self,
|
|
125
|
-
msg,
|
|
126
|
-
*args,
|
|
127
|
-
exc_info=None,
|
|
128
|
-
extra=None,
|
|
129
|
-
stack_info=False,
|
|
130
|
-
stacklevel=1,
|
|
131
|
-
|
|
132
|
-
):
|
|
129
|
+
msg: object,
|
|
130
|
+
*args: object,
|
|
131
|
+
exc_info: Any = None,
|
|
132
|
+
extra: dict[str, Any] | None = None,
|
|
133
|
+
stack_info: bool = False,
|
|
134
|
+
stacklevel: int = 1,
|
|
135
|
+
context: dict[str, Any] | None = None,
|
|
136
|
+
) -> None:
|
|
133
137
|
if self.isEnabledFor(logging.CRITICAL):
|
|
134
138
|
self._log(
|
|
135
139
|
logging.CRITICAL,
|
|
@@ -139,20 +143,20 @@ class AppLogger(logging.Logger):
|
|
|
139
143
|
extra=extra,
|
|
140
144
|
stack_info=stack_info,
|
|
141
145
|
stacklevel=stacklevel,
|
|
142
|
-
|
|
146
|
+
context=context,
|
|
143
147
|
)
|
|
144
148
|
|
|
145
|
-
def _log(
|
|
149
|
+
def _log( # type: ignore[override]
|
|
146
150
|
self,
|
|
147
|
-
level,
|
|
148
|
-
msg,
|
|
149
|
-
args,
|
|
150
|
-
exc_info=None,
|
|
151
|
-
extra=None,
|
|
152
|
-
stack_info=False,
|
|
153
|
-
stacklevel=1,
|
|
154
|
-
|
|
155
|
-
):
|
|
151
|
+
level: int,
|
|
152
|
+
msg: object,
|
|
153
|
+
args: tuple[object, ...],
|
|
154
|
+
exc_info: Any = None,
|
|
155
|
+
extra: dict[str, Any] | None = None,
|
|
156
|
+
stack_info: bool = False,
|
|
157
|
+
stacklevel: int = 1,
|
|
158
|
+
context: dict[str, Any] | None = None,
|
|
159
|
+
) -> None:
|
|
156
160
|
"""Low-level logging routine which creates a LogRecord and then calls all handlers."""
|
|
157
161
|
# Check if extra already has a 'context' key
|
|
158
162
|
if extra and "context" in extra:
|
|
@@ -163,9 +167,9 @@ class AppLogger(logging.Logger):
|
|
|
163
167
|
# Build final extra with context
|
|
164
168
|
extra = extra.copy() if extra else {}
|
|
165
169
|
|
|
166
|
-
# Add our context (persistent +
|
|
170
|
+
# Add our context (persistent + passed context) to extra["context"]
|
|
167
171
|
if self.context or context:
|
|
168
|
-
extra["context"] = {**self.context, **context}
|
|
172
|
+
extra["context"] = {**self.context, **(context or {})}
|
|
169
173
|
|
|
170
174
|
# Call the parent logger's _log method with explicit parameters
|
|
171
175
|
super()._log(
|
plain/logs/configure.py
CHANGED
|
@@ -1,36 +1,85 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import TextIO
|
|
2
4
|
|
|
5
|
+
from .filters import DebugInfoFilter, WarningErrorCriticalFilter
|
|
3
6
|
from .formatters import JSONFormatter, KeyValueFormatter
|
|
4
7
|
|
|
5
8
|
|
|
6
|
-
def
|
|
9
|
+
def attach_log_handlers(
|
|
10
|
+
*,
|
|
11
|
+
logger: logging.Logger,
|
|
12
|
+
info_stream: TextIO,
|
|
13
|
+
warning_stream: TextIO,
|
|
14
|
+
formatter: logging.Formatter,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Attach two handlers to a logger that split by log level.
|
|
17
|
+
|
|
18
|
+
INFO and below go to info_stream, WARNING and above go to warning_stream.
|
|
19
|
+
"""
|
|
20
|
+
# DEBUG and INFO handler
|
|
21
|
+
info_handler = logging.StreamHandler(info_stream)
|
|
22
|
+
info_handler.addFilter(DebugInfoFilter())
|
|
23
|
+
info_handler.setFormatter(formatter)
|
|
24
|
+
logger.addHandler(info_handler)
|
|
25
|
+
|
|
26
|
+
# WARNING, ERROR, and CRITICAL handler
|
|
27
|
+
warning_handler = logging.StreamHandler(warning_stream)
|
|
28
|
+
warning_handler.addFilter(WarningErrorCriticalFilter())
|
|
29
|
+
warning_handler.setFormatter(formatter)
|
|
30
|
+
logger.addHandler(warning_handler)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def configure_logging(
|
|
34
|
+
*,
|
|
35
|
+
plain_log_level: int | str,
|
|
36
|
+
app_log_level: int | str,
|
|
37
|
+
app_log_format: str,
|
|
38
|
+
log_stream: str = "split",
|
|
39
|
+
) -> None:
|
|
40
|
+
# Determine which streams to use based on log_stream setting
|
|
41
|
+
if log_stream == "split":
|
|
42
|
+
info_stream = sys.stdout
|
|
43
|
+
warning_stream = sys.stderr
|
|
44
|
+
elif log_stream == "stdout":
|
|
45
|
+
info_stream = sys.stdout
|
|
46
|
+
warning_stream = sys.stdout
|
|
47
|
+
else: # stderr (or any other value defaults to stderr for backwards compat)
|
|
48
|
+
info_stream = sys.stderr
|
|
49
|
+
warning_stream = sys.stderr
|
|
50
|
+
|
|
7
51
|
# Create and configure the plain logger (uses standard Logger, not AppLogger)
|
|
8
|
-
plain_logger = logging.
|
|
52
|
+
plain_logger = logging.getLogger("plain")
|
|
9
53
|
plain_logger.setLevel(plain_log_level)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
54
|
+
attach_log_handlers(
|
|
55
|
+
logger=plain_logger,
|
|
56
|
+
info_stream=info_stream,
|
|
57
|
+
warning_stream=warning_stream,
|
|
58
|
+
formatter=logging.Formatter("[%(levelname)s] %(message)s"),
|
|
59
|
+
)
|
|
13
60
|
plain_logger.propagate = False
|
|
14
|
-
logging.root.manager.loggerDict["plain"] = plain_logger
|
|
15
61
|
|
|
16
62
|
# Configure the existing app_logger
|
|
17
|
-
from .
|
|
63
|
+
from .app import app_logger
|
|
18
64
|
|
|
19
65
|
app_logger.setLevel(app_log_level)
|
|
20
66
|
app_logger.propagate = False
|
|
21
67
|
|
|
22
|
-
|
|
68
|
+
# Determine formatter based on app_log_format
|
|
23
69
|
match app_log_format:
|
|
24
70
|
case "json":
|
|
25
|
-
|
|
71
|
+
formatter = JSONFormatter("%(json)s")
|
|
26
72
|
case "keyvalue":
|
|
27
|
-
|
|
28
|
-
KeyValueFormatter("[%(levelname)s] %(message)s %(keyvalue)s")
|
|
29
|
-
)
|
|
73
|
+
formatter = KeyValueFormatter("[%(levelname)s] %(message)s %(keyvalue)s")
|
|
30
74
|
case _:
|
|
31
|
-
|
|
75
|
+
formatter = logging.Formatter("[%(levelname)s] %(message)s")
|
|
32
76
|
|
|
33
|
-
|
|
77
|
+
attach_log_handlers(
|
|
78
|
+
logger=app_logger,
|
|
79
|
+
info_stream=info_stream,
|
|
80
|
+
warning_stream=warning_stream,
|
|
81
|
+
formatter=formatter,
|
|
82
|
+
)
|
|
34
83
|
|
|
35
84
|
# Register the app_logger in the logging system so getLogger("app") returns it
|
|
36
85
|
logging.root.manager.loggerDict["app"] = app_logger
|