logging-objects-with-schema 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- logging_objects_with_schema-0.1.3/PKG-INFO +285 -0
- logging_objects_with_schema-0.1.3/README.md +252 -0
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/pyproject.toml +1 -1
- logging_objects_with_schema-0.1.3/src/logging_objects_with_schema/errors.py +66 -0
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/src/logging_objects_with_schema/schema_applier.py +103 -19
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/src/logging_objects_with_schema/schema_loader.py +130 -22
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/src/logging_objects_with_schema/schema_logger.py +81 -5
- logging_objects_with_schema-0.1.2/PKG-INFO +0 -449
- logging_objects_with_schema-0.1.2/README.md +0 -416
- logging_objects_with_schema-0.1.2/src/logging_objects_with_schema/errors.py +0 -27
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/LICENSE +0 -0
- {logging_objects_with_schema-0.1.2 → logging_objects_with_schema-0.1.3}/src/logging_objects_with_schema/__init__.py +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logging-objects-with-schema
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Proxy logging wrapper that validates extra fields against a JSON schema.
|
|
5
|
+
Keywords: logging,schema,validation,structured-logging,structured-logs
|
|
6
|
+
Author: Dmitrii Safronov
|
|
7
|
+
Author-email: Dmitrii Safronov <dhameoelin+logging-objects-with-schema@gmail.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Topic :: System :: Logging
|
|
19
|
+
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0.0 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: black>=23.0.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: isort>=5.12.0 ; extra == 'dev'
|
|
23
|
+
Requires-Dist: flake8>=6.0.0 ; extra == 'dev'
|
|
24
|
+
Requires-Dist: flake8-pyproject>=1.2.3 ; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.18.1 ; extra == 'dev'
|
|
26
|
+
Requires-Dist: bandit>=1.8.6 ; extra == 'dev'
|
|
27
|
+
Requires-Dist: pre-commit>=3.5.0 ; extra == 'dev'
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Project-URL: Issues, https://github.com/disafronov/python-logging-objects-with-schema/issues
|
|
30
|
+
Project-URL: Source, https://github.com/disafronov/python-logging-objects-with-schema
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# python-logging-objects-with-schema
|
|
35
|
+
|
|
36
|
+
This library provides a logger subclass built on top of the standard `logging`
|
|
37
|
+
module that strictly controls additional `extra` fields using a JSON schema.
|
|
38
|
+
`SchemaLogger` is a drop-in replacement for `logging.Logger` that validates
|
|
39
|
+
`extra` fields against a JSON schema file (`logging_objects_with_schema.json`)
|
|
40
|
+
in your application root directory.
|
|
41
|
+
|
|
42
|
+
## Schema as a contract
|
|
43
|
+
|
|
44
|
+
The JSON schema is treated as a **contract** between all parties that produce
|
|
45
|
+
and consume logs in the system. The schema file (`logging_objects_with_schema.json`)
|
|
46
|
+
is a shared, versioned artifact that defines which structured fields are allowed
|
|
47
|
+
to appear in logs and which Python types they must have. This contract ensures
|
|
48
|
+
that all downstream consumers (search systems, alerts, dashboards, external
|
|
49
|
+
systems) can rely on a consistent log structure.
|
|
50
|
+
|
|
51
|
+
**Strictness guarantees:**
|
|
52
|
+
|
|
53
|
+
- **`extra` fields never go directly into logs.** They are always projected
|
|
54
|
+
through the schema: values from `extra` are taken by `source` field names
|
|
55
|
+
and placed into the log structure according to the schema paths. Only fields
|
|
56
|
+
explicitly described in the schema (as leaves with `type` and `source`) can
|
|
57
|
+
ever reach your logs. The schema is the only source of truth for which
|
|
58
|
+
`extra` fields are allowed.
|
|
59
|
+
- Any `extra` field that is **not** described in the schema is treated as a
|
|
60
|
+
data error: it is dropped from the log output and recorded as a validation
|
|
61
|
+
problem.
|
|
62
|
+
- Any mismatch between runtime values and the declared types (wrong types,
|
|
63
|
+
`None` values, disallowed list elements) is also treated as a data error.
|
|
64
|
+
- All validation problems are aggregated and logged as a single ERROR message
|
|
65
|
+
**after** the log record has been emitted, ensuring 100% compatibility with
|
|
66
|
+
standard logger behavior (no exceptions are raised).
|
|
67
|
+
- Application code must only send `extra` fields that are described in the
|
|
68
|
+
schema and match the declared Python types. Any deviation from the schema
|
|
69
|
+
is considered a contract violation.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install logging-objects-with-schema
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Basic usage
|
|
78
|
+
|
|
79
|
+
### Quickstart (complete working example)
|
|
80
|
+
|
|
81
|
+
First, create a schema file `logging_objects_with_schema.json` in your application root:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"ServicePayload": {
|
|
86
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
87
|
+
"UserID": {"type": "int", "source": "user_id"}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then, set up and use the logger:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import logging
|
|
96
|
+
import sys
|
|
97
|
+
|
|
98
|
+
from logging_objects_with_schema import SchemaLogger
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Set SchemaLogger as the default logger class
|
|
102
|
+
logging.setLoggerClass(SchemaLogger)
|
|
103
|
+
|
|
104
|
+
# Configure handlers and formatters as usual
|
|
105
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
106
|
+
handler.setFormatter(logging.Formatter("%(message)s %(ServicePayload)s"))
|
|
107
|
+
|
|
108
|
+
# Get loggers using standard logging API
|
|
109
|
+
logger = logging.getLogger("service")
|
|
110
|
+
logger.setLevel(logging.INFO)
|
|
111
|
+
logger.addHandler(handler)
|
|
112
|
+
|
|
113
|
+
# Use the logger - extra fields are validated against the schema
|
|
114
|
+
logger.info("request processed", extra={"request_id": "abc-123", "user_id": 42})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Example with nested structures
|
|
118
|
+
|
|
119
|
+
Schema with nested structure:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"ServicePayload": {
|
|
124
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
125
|
+
"UserID": {"type": "int", "source": "user_id"},
|
|
126
|
+
"Metrics": {
|
|
127
|
+
"CPU": {"type": "float", "source": "cpu_usage"},
|
|
128
|
+
"Memory": {"type": "float", "source": "memory_usage"},
|
|
129
|
+
"Network": {
|
|
130
|
+
"In": {"type": "int", "source": "network_in"},
|
|
131
|
+
"Out": {"type": "int", "source": "network_out"}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
logger.info(
|
|
142
|
+
"metrics collected",
|
|
143
|
+
extra={
|
|
144
|
+
"request_id": "req-123",
|
|
145
|
+
"user_id": 42,
|
|
146
|
+
"cpu_usage": 75.5,
|
|
147
|
+
"memory_usage": 60.2,
|
|
148
|
+
"network_in": 1024,
|
|
149
|
+
"network_out": 2048,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error handling example
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from logging_objects_with_schema import SchemaLogger
|
|
158
|
+
|
|
159
|
+
# SchemaLogger is a drop-in replacement - no exception handling needed.
|
|
160
|
+
# If the schema has problems, the application will be terminated after
|
|
161
|
+
# logging schema problems to stderr.
|
|
162
|
+
logging.setLoggerClass(SchemaLogger)
|
|
163
|
+
logger = logging.getLogger("service")
|
|
164
|
+
|
|
165
|
+
# When logging with invalid data, validation errors are automatically
|
|
166
|
+
# logged as ERROR messages. No exception handling is needed.
|
|
167
|
+
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
168
|
+
# The valid part of the log is emitted, and validation errors are logged
|
|
169
|
+
# as ERROR messages in JSON format: {"validation_errors": [{"field": "...", "error": "...", "value": "..."}]}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Schema location and format
|
|
173
|
+
|
|
174
|
+
The schema file `logging_objects_with_schema.json` must be located in your
|
|
175
|
+
application root directory. The library searches upward from the current working
|
|
176
|
+
directory until it finds the file or reaches the filesystem root.
|
|
177
|
+
|
|
178
|
+
**Important**: If there are any problems with the schema (missing file, broken
|
|
179
|
+
JSON, invalid structure, etc.), the application is terminated after logging
|
|
180
|
+
schema problems to stderr. Schema validation happens when the first logger
|
|
181
|
+
instance is created.
|
|
182
|
+
|
|
183
|
+
**Note**: The schema is compiled once per process and cached. Schema changes
|
|
184
|
+
require an application restart to take effect. The library is thread-safe.
|
|
185
|
+
|
|
186
|
+
A valid empty schema (`{}`) is allowed and will result in no `extra` fields
|
|
187
|
+
being included in log records.
|
|
188
|
+
|
|
189
|
+
An example schema:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"ServicePayload": {
|
|
194
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
195
|
+
"UserID": {"type": "int", "source": "user_id"},
|
|
196
|
+
"Metrics": {
|
|
197
|
+
"CPU": {"type": "float", "source": "cpu_usage"},
|
|
198
|
+
"Memory": {"type": "float", "source": "memory_usage"},
|
|
199
|
+
"Network": {
|
|
200
|
+
"In": {"type": "int", "source": "network_in"},
|
|
201
|
+
"Out": {"type": "int", "source": "network_out"}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
An example of a valid empty schema (no leaves, no problems):
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Schema structure:**
|
|
215
|
+
|
|
216
|
+
- **Inner nodes**: Objects without `type` and `source` fields (used for nesting).
|
|
217
|
+
- **Leaf nodes**: Objects with both `type` and `source` fields. A valid leaf
|
|
218
|
+
must have both fields present and non-empty.
|
|
219
|
+
- **`type`**: One of `"str"`, `"int"`, `"float"`, `"bool"`, or `"list"`.
|
|
220
|
+
- **`source`**: The name of the field in `extra` from which the value is taken.
|
|
221
|
+
- **Root key restrictions**: Root keys cannot conflict with standard `logging`
|
|
222
|
+
module fields (e.g., `name`, `levelno`, `pathname`). Such conflicts cause
|
|
223
|
+
schema validation to fail.
|
|
224
|
+
|
|
225
|
+
**List-typed fields:**
|
|
226
|
+
|
|
227
|
+
For `"type": "list"`, you must also specify `"item_type"` (one of `"str"`,
|
|
228
|
+
`"int"`, `"float"`, `"bool"`). Lists must contain homogeneous primitive elements
|
|
229
|
+
of the declared type. Empty lists are allowed; nested lists and dictionaries are
|
|
230
|
+
not permitted.
|
|
231
|
+
|
|
232
|
+
Example of a valid list field:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"ServicePayload": {
|
|
237
|
+
"Tags": {
|
|
238
|
+
"type": "list",
|
|
239
|
+
"source": "tags",
|
|
240
|
+
"item_type": "str"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Usage:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
logger.info(
|
|
250
|
+
"request processed",
|
|
251
|
+
extra={
|
|
252
|
+
"tags": ["blue", "fast", "cached"], # list[str] – valid
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Invalid example (non-primitive elements are rejected):
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
logger.info("request processed", extra={"tags": [{"key": "color"}]}) # Invalid
|
|
261
|
+
# Validation error is logged as ERROR after the log record is emitted
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Multiple leaves with the same source:**
|
|
265
|
+
|
|
266
|
+
A single `source` field name can be used in multiple leaves. The value is
|
|
267
|
+
validated independently for each leaf and written to all matching locations.
|
|
268
|
+
If types conflict, the value is written only where the type matches, and
|
|
269
|
+
validation errors are reported for mismatched locations.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"ServicePayload": {
|
|
276
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
277
|
+
"Metadata": {
|
|
278
|
+
"ID": {"type": "str", "source": "request_id"}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
With `extra={"request_id": "abc-123"}`, the value appears in both
|
|
285
|
+
`ServicePayload.RequestID` and `ServicePayload.Metadata.ID`.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# python-logging-objects-with-schema
|
|
2
|
+
|
|
3
|
+
This library provides a logger subclass built on top of the standard `logging`
|
|
4
|
+
module that strictly controls additional `extra` fields using a JSON schema.
|
|
5
|
+
`SchemaLogger` is a drop-in replacement for `logging.Logger` that validates
|
|
6
|
+
`extra` fields against a JSON schema file (`logging_objects_with_schema.json`)
|
|
7
|
+
in your application root directory.
|
|
8
|
+
|
|
9
|
+
## Schema as a contract
|
|
10
|
+
|
|
11
|
+
The JSON schema is treated as a **contract** between all parties that produce
|
|
12
|
+
and consume logs in the system. The schema file (`logging_objects_with_schema.json`)
|
|
13
|
+
is a shared, versioned artifact that defines which structured fields are allowed
|
|
14
|
+
to appear in logs and which Python types they must have. This contract ensures
|
|
15
|
+
that all downstream consumers (search systems, alerts, dashboards, external
|
|
16
|
+
systems) can rely on a consistent log structure.
|
|
17
|
+
|
|
18
|
+
**Strictness guarantees:**
|
|
19
|
+
|
|
20
|
+
- **`extra` fields never go directly into logs.** They are always projected
|
|
21
|
+
through the schema: values from `extra` are taken by `source` field names
|
|
22
|
+
and placed into the log structure according to the schema paths. Only fields
|
|
23
|
+
explicitly described in the schema (as leaves with `type` and `source`) can
|
|
24
|
+
ever reach your logs. The schema is the only source of truth for which
|
|
25
|
+
`extra` fields are allowed.
|
|
26
|
+
- Any `extra` field that is **not** described in the schema is treated as a
|
|
27
|
+
data error: it is dropped from the log output and recorded as a validation
|
|
28
|
+
problem.
|
|
29
|
+
- Any mismatch between runtime values and the declared types (wrong types,
|
|
30
|
+
`None` values, disallowed list elements) is also treated as a data error.
|
|
31
|
+
- All validation problems are aggregated and logged as a single ERROR message
|
|
32
|
+
**after** the log record has been emitted, ensuring 100% compatibility with
|
|
33
|
+
standard logger behavior (no exceptions are raised).
|
|
34
|
+
- Application code must only send `extra` fields that are described in the
|
|
35
|
+
schema and match the declared Python types. Any deviation from the schema
|
|
36
|
+
is considered a contract violation.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install logging-objects-with-schema
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Basic usage
|
|
45
|
+
|
|
46
|
+
### Quickstart (complete working example)
|
|
47
|
+
|
|
48
|
+
First, create a schema file `logging_objects_with_schema.json` in your application root:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"ServicePayload": {
|
|
53
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
54
|
+
"UserID": {"type": "int", "source": "user_id"}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then, set up and use the logger:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import logging
|
|
63
|
+
import sys
|
|
64
|
+
|
|
65
|
+
from logging_objects_with_schema import SchemaLogger
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Set SchemaLogger as the default logger class
|
|
69
|
+
logging.setLoggerClass(SchemaLogger)
|
|
70
|
+
|
|
71
|
+
# Configure handlers and formatters as usual
|
|
72
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
73
|
+
handler.setFormatter(logging.Formatter("%(message)s %(ServicePayload)s"))
|
|
74
|
+
|
|
75
|
+
# Get loggers using standard logging API
|
|
76
|
+
logger = logging.getLogger("service")
|
|
77
|
+
logger.setLevel(logging.INFO)
|
|
78
|
+
logger.addHandler(handler)
|
|
79
|
+
|
|
80
|
+
# Use the logger - extra fields are validated against the schema
|
|
81
|
+
logger.info("request processed", extra={"request_id": "abc-123", "user_id": 42})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Example with nested structures
|
|
85
|
+
|
|
86
|
+
Schema with nested structure:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"ServicePayload": {
|
|
91
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
92
|
+
"UserID": {"type": "int", "source": "user_id"},
|
|
93
|
+
"Metrics": {
|
|
94
|
+
"CPU": {"type": "float", "source": "cpu_usage"},
|
|
95
|
+
"Memory": {"type": "float", "source": "memory_usage"},
|
|
96
|
+
"Network": {
|
|
97
|
+
"In": {"type": "int", "source": "network_in"},
|
|
98
|
+
"Out": {"type": "int", "source": "network_out"}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
logger.info(
|
|
109
|
+
"metrics collected",
|
|
110
|
+
extra={
|
|
111
|
+
"request_id": "req-123",
|
|
112
|
+
"user_id": 42,
|
|
113
|
+
"cpu_usage": 75.5,
|
|
114
|
+
"memory_usage": 60.2,
|
|
115
|
+
"network_in": 1024,
|
|
116
|
+
"network_out": 2048,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Error handling example
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from logging_objects_with_schema import SchemaLogger
|
|
125
|
+
|
|
126
|
+
# SchemaLogger is a drop-in replacement - no exception handling needed.
|
|
127
|
+
# If the schema has problems, the application will be terminated after
|
|
128
|
+
# logging schema problems to stderr.
|
|
129
|
+
logging.setLoggerClass(SchemaLogger)
|
|
130
|
+
logger = logging.getLogger("service")
|
|
131
|
+
|
|
132
|
+
# When logging with invalid data, validation errors are automatically
|
|
133
|
+
# logged as ERROR messages. No exception handling is needed.
|
|
134
|
+
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
135
|
+
# The valid part of the log is emitted, and validation errors are logged
|
|
136
|
+
# as ERROR messages in JSON format: {"validation_errors": [{"field": "...", "error": "...", "value": "..."}]}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Schema location and format
|
|
140
|
+
|
|
141
|
+
The schema file `logging_objects_with_schema.json` must be located in your
|
|
142
|
+
application root directory. The library searches upward from the current working
|
|
143
|
+
directory until it finds the file or reaches the filesystem root.
|
|
144
|
+
|
|
145
|
+
**Important**: If there are any problems with the schema (missing file, broken
|
|
146
|
+
JSON, invalid structure, etc.), the application is terminated after logging
|
|
147
|
+
schema problems to stderr. Schema validation happens when the first logger
|
|
148
|
+
instance is created.
|
|
149
|
+
|
|
150
|
+
**Note**: The schema is compiled once per process and cached. Schema changes
|
|
151
|
+
require an application restart to take effect. The library is thread-safe.
|
|
152
|
+
|
|
153
|
+
A valid empty schema (`{}`) is allowed and will result in no `extra` fields
|
|
154
|
+
being included in log records.
|
|
155
|
+
|
|
156
|
+
An example schema:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"ServicePayload": {
|
|
161
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
162
|
+
"UserID": {"type": "int", "source": "user_id"},
|
|
163
|
+
"Metrics": {
|
|
164
|
+
"CPU": {"type": "float", "source": "cpu_usage"},
|
|
165
|
+
"Memory": {"type": "float", "source": "memory_usage"},
|
|
166
|
+
"Network": {
|
|
167
|
+
"In": {"type": "int", "source": "network_in"},
|
|
168
|
+
"Out": {"type": "int", "source": "network_out"}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
An example of a valid empty schema (no leaves, no problems):
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Schema structure:**
|
|
182
|
+
|
|
183
|
+
- **Inner nodes**: Objects without `type` and `source` fields (used for nesting).
|
|
184
|
+
- **Leaf nodes**: Objects with both `type` and `source` fields. A valid leaf
|
|
185
|
+
must have both fields present and non-empty.
|
|
186
|
+
- **`type`**: One of `"str"`, `"int"`, `"float"`, `"bool"`, or `"list"`.
|
|
187
|
+
- **`source`**: The name of the field in `extra` from which the value is taken.
|
|
188
|
+
- **Root key restrictions**: Root keys cannot conflict with standard `logging`
|
|
189
|
+
module fields (e.g., `name`, `levelno`, `pathname`). Such conflicts cause
|
|
190
|
+
schema validation to fail.
|
|
191
|
+
|
|
192
|
+
**List-typed fields:**
|
|
193
|
+
|
|
194
|
+
For `"type": "list"`, you must also specify `"item_type"` (one of `"str"`,
|
|
195
|
+
`"int"`, `"float"`, `"bool"`). Lists must contain homogeneous primitive elements
|
|
196
|
+
of the declared type. Empty lists are allowed; nested lists and dictionaries are
|
|
197
|
+
not permitted.
|
|
198
|
+
|
|
199
|
+
Example of a valid list field:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"ServicePayload": {
|
|
204
|
+
"Tags": {
|
|
205
|
+
"type": "list",
|
|
206
|
+
"source": "tags",
|
|
207
|
+
"item_type": "str"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Usage:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
logger.info(
|
|
217
|
+
"request processed",
|
|
218
|
+
extra={
|
|
219
|
+
"tags": ["blue", "fast", "cached"], # list[str] – valid
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Invalid example (non-primitive elements are rejected):
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
logger.info("request processed", extra={"tags": [{"key": "color"}]}) # Invalid
|
|
228
|
+
# Validation error is logged as ERROR after the log record is emitted
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Multiple leaves with the same source:**
|
|
232
|
+
|
|
233
|
+
A single `source` field name can be used in multiple leaves. The value is
|
|
234
|
+
validated independently for each leaf and written to all matching locations.
|
|
235
|
+
If types conflict, the value is written only where the type matches, and
|
|
236
|
+
validation errors are reported for mismatched locations.
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"ServicePayload": {
|
|
243
|
+
"RequestID": {"type": "str", "source": "request_id"},
|
|
244
|
+
"Metadata": {
|
|
245
|
+
"ID": {"type": "str", "source": "request_id"}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
With `extra={"request_id": "abc-123"}`, the value appears in both
|
|
252
|
+
`ServicePayload.RequestID` and `ServicePayload.Metadata.ID`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Custom exception types used by logging_objects_with_schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SchemaProblem:
|
|
10
|
+
"""Describes a single problem encountered while loading the schema.
|
|
11
|
+
|
|
12
|
+
This class is used to report schema validation errors during schema
|
|
13
|
+
compilation. Schema problems are fatal: if any are detected during
|
|
14
|
+
logger initialization, the application is terminated after logging
|
|
15
|
+
all problems to stderr via ``os._exit(1)``.
|
|
16
|
+
|
|
17
|
+
Schema problems can occur due to:
|
|
18
|
+
- Missing or inaccessible schema file
|
|
19
|
+
- Invalid JSON syntax
|
|
20
|
+
- Invalid schema structure (non-object top-level, missing required fields)
|
|
21
|
+
- Unknown type names or invalid type declarations
|
|
22
|
+
- Root key conflicts with reserved logging fields
|
|
23
|
+
- Excessive nesting depth (exceeds MAX_SCHEMA_DEPTH)
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
message: Human-readable description of the problem. Examples include:
|
|
27
|
+
- "Schema file not found: /path/to/logging_objects_with_schema.json"
|
|
28
|
+
- "Failed to parse JSON schema: Expecting ',' delimiter: line 5 column 10"
|
|
29
|
+
- "Unknown type 'string' at ServicePayload.RequestID"
|
|
30
|
+
- "Root key 'name' conflicts with reserved logging fields"
|
|
31
|
+
- "Schema nesting depth exceeds maximum allowed depth of 100 at "
|
|
32
|
+
"path ServicePayload.Metrics"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
message: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DataProblem:
|
|
40
|
+
"""Describes a single problem encountered while validating log data.
|
|
41
|
+
|
|
42
|
+
This class is used to report validation errors when applying the compiled
|
|
43
|
+
schema to user-provided ``extra`` fields during logging. Unlike
|
|
44
|
+
:class:`SchemaProblem`, data problems are not fatal: they are collected
|
|
45
|
+
and logged as ERROR messages *after* the main log record has been emitted,
|
|
46
|
+
ensuring 100% compatibility with standard logger behavior.
|
|
47
|
+
|
|
48
|
+
Data problems can occur due to:
|
|
49
|
+
- Type mismatches (e.g., providing str where int is expected)
|
|
50
|
+
- None values (None is never allowed for any type)
|
|
51
|
+
- Invalid list elements (non-homogeneous lists, non-primitive elements)
|
|
52
|
+
- Redundant fields (fields not defined in the schema)
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
message: JSON string containing structured error information. The message
|
|
56
|
+
is always a valid JSON object with the following structure:
|
|
57
|
+
``{"field": "...", "error": "...", "value": "..."}``
|
|
58
|
+
All values are serialized via ``repr()`` for safety and consistency.
|
|
59
|
+
Examples:
|
|
60
|
+
- ``{"field": "'user_id'", "error": "'has type str, expected int'", "value": "'abc-123'"}``
|
|
61
|
+
- ``{"field": "'request_id'", "error": "'is None'", "value": "None"}``
|
|
62
|
+
- ``{"field": "'tags'", "error": "'is a list but contains elements with types dict; expected all elements to be of type str'", "value": "[{'key': 'color'}]"}`` # noqa: E501
|
|
63
|
+
- ``{"field": "'unknown_field'", "error": "'is not defined in schema'", "value": "'some_value'"}``
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
message: str
|