logging-objects-with-schema 0.1.1__tar.gz → 0.1.2__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.1 → logging_objects_with_schema-0.1.2}/PKG-INFO +66 -79
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/README.md +65 -78
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/pyproject.toml +1 -1
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/src/logging_objects_with_schema/__init__.py +0 -3
- logging_objects_with_schema-0.1.2/src/logging_objects_with_schema/errors.py +27 -0
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/src/logging_objects_with_schema/schema_applier.py +7 -7
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/src/logging_objects_with_schema/schema_loader.py +35 -28
- logging_objects_with_schema-0.1.2/src/logging_objects_with_schema/schema_logger.py +207 -0
- logging_objects_with_schema-0.1.1/src/logging_objects_with_schema/errors.py +0 -84
- logging_objects_with_schema-0.1.1/src/logging_objects_with_schema/schema_logger.py +0 -139
- {logging_objects_with_schema-0.1.1 → logging_objects_with_schema-0.1.2}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logging-objects-with-schema
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Proxy logging wrapper that validates extra fields against a JSON schema.
|
|
5
5
|
Keywords: logging,schema,validation,structured-logging,structured-logs
|
|
6
6
|
Author: Dmitrii Safronov
|
|
@@ -57,7 +57,7 @@ to appear in logs and which types they must have.
|
|
|
57
57
|
|
|
58
58
|
- Application code must only send `extra` fields that are described in the schema
|
|
59
59
|
and match the declared Python types. Any deviation (unknown fields, wrong types,
|
|
60
|
-
`None` values, disallowed list elements) is
|
|
60
|
+
`None` values, disallowed list elements) is logged as an ERROR message
|
|
61
61
|
*after* the log record has been emitted.
|
|
62
62
|
- The schema file (`logging_objects_with_schema.json`) is a shared, versioned
|
|
63
63
|
artifact that defines the shape of structured log payloads for all downstream
|
|
@@ -72,9 +72,9 @@ to appear in logs and which types they must have.
|
|
|
72
72
|
- Any mismatch between runtime values and the declared types is also treated as
|
|
73
73
|
a data error.
|
|
74
74
|
- All validation problems (unknown fields, wrong types, disallowed list
|
|
75
|
-
elements, `None` values, etc.) are aggregated
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
elements, `None` values, etc.) are aggregated and logged as a single
|
|
76
|
+
ERROR message **after** the log record has been emitted, ensuring 100%
|
|
77
|
+
compatibility with standard logger behavior (no exceptions are raised).
|
|
78
78
|
- The schema is treated as the only source of truth for which `extra` fields
|
|
79
79
|
are allowed to appear in logs. Any deviation from the schema is considered a
|
|
80
80
|
contract violation between the producer of `extra` and the schema author.
|
|
@@ -165,34 +165,19 @@ logger.info(
|
|
|
165
165
|
### Error handling example
|
|
166
166
|
|
|
167
167
|
```python
|
|
168
|
-
from logging_objects_with_schema import SchemaLogger
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# System-level errors (e.g., inaccessible working directory when os.getcwd()
|
|
182
|
-
# fails, lock failures, path resolution issues) are raised directly, not
|
|
183
|
-
# wrapped in SchemaValidationError. Note: OSError when reading the schema
|
|
184
|
-
# file is converted to SchemaValidationError and handled above.
|
|
185
|
-
print(f"System error during logger initialization: {e}")
|
|
186
|
-
raise
|
|
187
|
-
|
|
188
|
-
# When logging, handle DataValidationError
|
|
189
|
-
try:
|
|
190
|
-
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
191
|
-
except DataValidationError as e:
|
|
192
|
-
print(f"Data validation failed: {e}")
|
|
193
|
-
for problem in e.problems:
|
|
194
|
-
print(f" - {problem.message}")
|
|
195
|
-
# Note: the valid part of the log was already emitted before the exception
|
|
168
|
+
from logging_objects_with_schema import SchemaLogger
|
|
169
|
+
|
|
170
|
+
# SchemaLogger is a drop-in replacement - no exception handling needed.
|
|
171
|
+
# If the schema has problems, the application will be terminated after
|
|
172
|
+
# logging schema problems to stderr.
|
|
173
|
+
logging.setLoggerClass(SchemaLogger)
|
|
174
|
+
logger = logging.getLogger("service")
|
|
175
|
+
|
|
176
|
+
# When logging with invalid data, validation errors are automatically
|
|
177
|
+
# logged as ERROR messages. No exception handling is needed.
|
|
178
|
+
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
179
|
+
# The valid part of the log is emitted, and validation errors are logged
|
|
180
|
+
# as ERROR messages with details about the problems.
|
|
196
181
|
```
|
|
197
182
|
|
|
198
183
|
### API compatibility with ``logging.Logger``
|
|
@@ -214,12 +199,11 @@ except DataValidationError as e:
|
|
|
214
199
|
- Schema tree depth is limited to a maximum nesting level (currently 100). Any
|
|
215
200
|
branch that exceeds this depth is ignored and reported as a schema problem.
|
|
216
201
|
|
|
217
|
-
If the schema file is not found, or cannot be read/parsed/validated,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
working directory at schema discovery time).
|
|
202
|
+
If the schema file is not found, or cannot be read/parsed/validated, the
|
|
203
|
+
logger instance is not created, schema problems are logged to stderr, and
|
|
204
|
+
the application is terminated via `os._exit(1)`. The error message contains
|
|
205
|
+
a detailed description of all issues, including the path where the file was
|
|
206
|
+
expected (based on the current working directory at schema discovery time).
|
|
223
207
|
|
|
224
208
|
An example schema:
|
|
225
209
|
|
|
@@ -240,8 +224,21 @@ An example schema:
|
|
|
240
224
|
}
|
|
241
225
|
```
|
|
242
226
|
|
|
243
|
-
|
|
244
|
-
|
|
227
|
+
An example of a valid empty schema (no leaves, no problems):
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
An empty schema is valid and does not cause errors. When using an empty schema,
|
|
234
|
+
no `extra` fields will be included in log records, and any attempt to log with
|
|
235
|
+
`extra` fields will result in validation errors being logged as ERROR messages.
|
|
236
|
+
|
|
237
|
+
- An inner node is an object without `type` and `source` (neither field is present).
|
|
238
|
+
- A leaf node is an object that has at least one of `type` or `source` fields.
|
|
239
|
+
However, a valid leaf node must have both `type` and `source` fields. If a
|
|
240
|
+
leaf node is missing either field or has an empty value, it will be reported
|
|
241
|
+
as a schema problem during validation.
|
|
245
242
|
- `type` is one of the allowed Python type names: `"str"`, `"int"`, `"float"`,
|
|
246
243
|
`"bool"`, or `"list"`.
|
|
247
244
|
- For `"list"` type, an additional `item_type` field is required to declare
|
|
@@ -303,7 +300,8 @@ message similar to:
|
|
|
303
300
|
|
|
304
301
|
> Field 'tags' is a list but contains elements with types [...]; expected all elements to be of type str
|
|
305
302
|
|
|
306
|
-
and
|
|
303
|
+
and an ERROR message is logged **after** the log record has been emitted with
|
|
304
|
+
the format: `"Log data does not match schema: {problem1}; {problem2}; ..."`.
|
|
307
305
|
|
|
308
306
|
### Multiple leaves with the same source
|
|
309
307
|
|
|
@@ -317,7 +315,7 @@ When a `source` is referenced by multiple leaves:
|
|
|
317
315
|
- The value is written only to those leaf locations where the runtime type
|
|
318
316
|
matches the expected type.
|
|
319
317
|
- For leaf locations where the type does not match, a `DataProblem` is added
|
|
320
|
-
to the
|
|
318
|
+
to the ERROR message that is logged after logging.
|
|
321
319
|
|
|
322
320
|
Example schema with duplicate source usage:
|
|
323
321
|
|
|
@@ -353,30 +351,23 @@ consistent type expectations when reusing a `source` field.
|
|
|
353
351
|
- If there are **any** problems with the schema (missing file, broken JSON,
|
|
354
352
|
invalid `type` values, conflicting root fields that match system logging
|
|
355
353
|
fields, malformed structure, etc.):
|
|
356
|
-
-
|
|
357
|
-
|
|
354
|
+
- the logger instance is not created (schema validation happens before
|
|
355
|
+
the logger is initialized);
|
|
356
|
+
- schema problems are logged to stderr in the format:
|
|
357
|
+
`"Schema has problems: {problem1}; {problem2}; ..."`;
|
|
358
|
+
- the application is terminated via `os._exit(1)`.
|
|
358
359
|
- If there are no problems:
|
|
359
360
|
- the schema is compiled into a `CompiledSchema`;
|
|
360
361
|
- the logger is created and starts using this schema to validate `extra`
|
|
361
362
|
fields.
|
|
363
|
+
- A valid empty schema (e.g., `{}` or a schema with only inner nodes and no
|
|
364
|
+
leaves) is treated as valid and does not cause errors. The logger is created
|
|
365
|
+
successfully, but no `extra` fields will be included in log records.
|
|
362
366
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
being wrapped in `SchemaValidationError`. Specifically:
|
|
368
|
-
|
|
369
|
-
- `OSError` when the current working directory is inaccessible or deleted (e.g.,
|
|
370
|
-
when `os.getcwd()` fails) is propagated as-is to indicate environmental issues.
|
|
371
|
-
However, `OSError` that occurs when reading the schema file (e.g., permission
|
|
372
|
-
denied, I/O errors) is converted to `SchemaValidationError` and reported as a
|
|
373
|
-
schema problem.
|
|
374
|
-
- `ValueError` when path resolution fails due to invalid characters or malformed
|
|
375
|
-
paths during schema file discovery is propagated as-is.
|
|
376
|
-
- `RuntimeError` when thread lock acquisition fails is propagated as-is.
|
|
377
|
-
|
|
378
|
-
Applications should handle these exceptions separately if they need to
|
|
379
|
-
distinguish between schema validation failures and system-level errors.
|
|
367
|
+
**Note**: System-level errors (OSError, ValueError, RuntimeError) that occur
|
|
368
|
+
during schema compilation are converted to `SchemaProblem` instances and
|
|
369
|
+
handled the same way as schema validation problems - the application is
|
|
370
|
+
terminated after logging the error to stderr.
|
|
380
371
|
|
|
381
372
|
## Schema caching and thread safety
|
|
382
373
|
|
|
@@ -428,8 +419,9 @@ distinguish between schema validation failures and system-level errors.
|
|
|
428
419
|
**it is simply not included in the final log record**.
|
|
429
420
|
|
|
430
421
|
In all of these cases a `DataProblem` is recorded for each offending field, and
|
|
431
|
-
if at least one problem is present a single
|
|
432
|
-
**after** the log record has been emitted.
|
|
422
|
+
if at least one problem is present, a single ERROR message is logged
|
|
423
|
+
**after** the log record has been emitted. The error message format is:
|
|
424
|
+
`"Log data does not match schema: {problem1}; {problem2}; ..."`.
|
|
433
425
|
|
|
434
426
|
- When a `source` is used in multiple leaves (see "Multiple leaves with the same
|
|
435
427
|
source" above), the value is validated and written independently for each leaf
|
|
@@ -445,18 +437,13 @@ High-level algorithm inside `SchemaLogger`:
|
|
|
445
437
|
2. A new structured payload is built from the schema and the given `extra`.
|
|
446
438
|
3. Only this structured payload is passed to the underlying stdlib logger.
|
|
447
439
|
4. After logging, if any validation problems were detected, a single
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
-
|
|
457
|
-
|
|
458
|
-
- the runtime type does not match the expected type;
|
|
459
|
-
- a list contains non-primitive elements (only str, int, float, bool allowed);
|
|
460
|
-
- the field is redundant and not described in the schema.
|
|
461
|
-
- Raised **after** the valid part of the payload has been logged.
|
|
462
|
-
- Contains a `problems` list describing the offending fields.
|
|
440
|
+
ERROR message is logged with the format:
|
|
441
|
+
`"Log data does not match schema: {problem1}; {problem2}; ..."`
|
|
442
|
+
(no exception is raised, ensuring 100% compatibility with standard logger behavior).
|
|
443
|
+
|
|
444
|
+
## Error handling
|
|
445
|
+
|
|
446
|
+
- Schema problems are handled internally: errors are logged to stderr and
|
|
447
|
+
the application is terminated via `os._exit(1)`.
|
|
448
|
+
- No exceptions are raised by `SchemaLogger` during initialization, making
|
|
449
|
+
it a true drop-in replacement for `logging.Logger`.
|
|
@@ -24,7 +24,7 @@ to appear in logs and which types they must have.
|
|
|
24
24
|
|
|
25
25
|
- Application code must only send `extra` fields that are described in the schema
|
|
26
26
|
and match the declared Python types. Any deviation (unknown fields, wrong types,
|
|
27
|
-
`None` values, disallowed list elements) is
|
|
27
|
+
`None` values, disallowed list elements) is logged as an ERROR message
|
|
28
28
|
*after* the log record has been emitted.
|
|
29
29
|
- The schema file (`logging_objects_with_schema.json`) is a shared, versioned
|
|
30
30
|
artifact that defines the shape of structured log payloads for all downstream
|
|
@@ -39,9 +39,9 @@ to appear in logs and which types they must have.
|
|
|
39
39
|
- Any mismatch between runtime values and the declared types is also treated as
|
|
40
40
|
a data error.
|
|
41
41
|
- All validation problems (unknown fields, wrong types, disallowed list
|
|
42
|
-
elements, `None` values, etc.) are aggregated
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
elements, `None` values, etc.) are aggregated and logged as a single
|
|
43
|
+
ERROR message **after** the log record has been emitted, ensuring 100%
|
|
44
|
+
compatibility with standard logger behavior (no exceptions are raised).
|
|
45
45
|
- The schema is treated as the only source of truth for which `extra` fields
|
|
46
46
|
are allowed to appear in logs. Any deviation from the schema is considered a
|
|
47
47
|
contract violation between the producer of `extra` and the schema author.
|
|
@@ -132,34 +132,19 @@ logger.info(
|
|
|
132
132
|
### Error handling example
|
|
133
133
|
|
|
134
134
|
```python
|
|
135
|
-
from logging_objects_with_schema import SchemaLogger
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# System-level errors (e.g., inaccessible working directory when os.getcwd()
|
|
149
|
-
# fails, lock failures, path resolution issues) are raised directly, not
|
|
150
|
-
# wrapped in SchemaValidationError. Note: OSError when reading the schema
|
|
151
|
-
# file is converted to SchemaValidationError and handled above.
|
|
152
|
-
print(f"System error during logger initialization: {e}")
|
|
153
|
-
raise
|
|
154
|
-
|
|
155
|
-
# When logging, handle DataValidationError
|
|
156
|
-
try:
|
|
157
|
-
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
158
|
-
except DataValidationError as e:
|
|
159
|
-
print(f"Data validation failed: {e}")
|
|
160
|
-
for problem in e.problems:
|
|
161
|
-
print(f" - {problem.message}")
|
|
162
|
-
# Note: the valid part of the log was already emitted before the exception
|
|
135
|
+
from logging_objects_with_schema import SchemaLogger
|
|
136
|
+
|
|
137
|
+
# SchemaLogger is a drop-in replacement - no exception handling needed.
|
|
138
|
+
# If the schema has problems, the application will be terminated after
|
|
139
|
+
# logging schema problems to stderr.
|
|
140
|
+
logging.setLoggerClass(SchemaLogger)
|
|
141
|
+
logger = logging.getLogger("service")
|
|
142
|
+
|
|
143
|
+
# When logging with invalid data, validation errors are automatically
|
|
144
|
+
# logged as ERROR messages. No exception handling is needed.
|
|
145
|
+
logger.info("processing", extra={"user_id": "not-an-int"}) # Wrong type
|
|
146
|
+
# The valid part of the log is emitted, and validation errors are logged
|
|
147
|
+
# as ERROR messages with details about the problems.
|
|
163
148
|
```
|
|
164
149
|
|
|
165
150
|
### API compatibility with ``logging.Logger``
|
|
@@ -181,12 +166,11 @@ except DataValidationError as e:
|
|
|
181
166
|
- Schema tree depth is limited to a maximum nesting level (currently 100). Any
|
|
182
167
|
branch that exceeds this depth is ignored and reported as a schema problem.
|
|
183
168
|
|
|
184
|
-
If the schema file is not found, or cannot be read/parsed/validated,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
working directory at schema discovery time).
|
|
169
|
+
If the schema file is not found, or cannot be read/parsed/validated, the
|
|
170
|
+
logger instance is not created, schema problems are logged to stderr, and
|
|
171
|
+
the application is terminated via `os._exit(1)`. The error message contains
|
|
172
|
+
a detailed description of all issues, including the path where the file was
|
|
173
|
+
expected (based on the current working directory at schema discovery time).
|
|
190
174
|
|
|
191
175
|
An example schema:
|
|
192
176
|
|
|
@@ -207,8 +191,21 @@ An example schema:
|
|
|
207
191
|
}
|
|
208
192
|
```
|
|
209
193
|
|
|
210
|
-
|
|
211
|
-
|
|
194
|
+
An example of a valid empty schema (no leaves, no problems):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
An empty schema is valid and does not cause errors. When using an empty schema,
|
|
201
|
+
no `extra` fields will be included in log records, and any attempt to log with
|
|
202
|
+
`extra` fields will result in validation errors being logged as ERROR messages.
|
|
203
|
+
|
|
204
|
+
- An inner node is an object without `type` and `source` (neither field is present).
|
|
205
|
+
- A leaf node is an object that has at least one of `type` or `source` fields.
|
|
206
|
+
However, a valid leaf node must have both `type` and `source` fields. If a
|
|
207
|
+
leaf node is missing either field or has an empty value, it will be reported
|
|
208
|
+
as a schema problem during validation.
|
|
212
209
|
- `type` is one of the allowed Python type names: `"str"`, `"int"`, `"float"`,
|
|
213
210
|
`"bool"`, or `"list"`.
|
|
214
211
|
- For `"list"` type, an additional `item_type` field is required to declare
|
|
@@ -270,7 +267,8 @@ message similar to:
|
|
|
270
267
|
|
|
271
268
|
> Field 'tags' is a list but contains elements with types [...]; expected all elements to be of type str
|
|
272
269
|
|
|
273
|
-
and
|
|
270
|
+
and an ERROR message is logged **after** the log record has been emitted with
|
|
271
|
+
the format: `"Log data does not match schema: {problem1}; {problem2}; ..."`.
|
|
274
272
|
|
|
275
273
|
### Multiple leaves with the same source
|
|
276
274
|
|
|
@@ -284,7 +282,7 @@ When a `source` is referenced by multiple leaves:
|
|
|
284
282
|
- The value is written only to those leaf locations where the runtime type
|
|
285
283
|
matches the expected type.
|
|
286
284
|
- For leaf locations where the type does not match, a `DataProblem` is added
|
|
287
|
-
to the
|
|
285
|
+
to the ERROR message that is logged after logging.
|
|
288
286
|
|
|
289
287
|
Example schema with duplicate source usage:
|
|
290
288
|
|
|
@@ -320,30 +318,23 @@ consistent type expectations when reusing a `source` field.
|
|
|
320
318
|
- If there are **any** problems with the schema (missing file, broken JSON,
|
|
321
319
|
invalid `type` values, conflicting root fields that match system logging
|
|
322
320
|
fields, malformed structure, etc.):
|
|
323
|
-
-
|
|
324
|
-
|
|
321
|
+
- the logger instance is not created (schema validation happens before
|
|
322
|
+
the logger is initialized);
|
|
323
|
+
- schema problems are logged to stderr in the format:
|
|
324
|
+
`"Schema has problems: {problem1}; {problem2}; ..."`;
|
|
325
|
+
- the application is terminated via `os._exit(1)`.
|
|
325
326
|
- If there are no problems:
|
|
326
327
|
- the schema is compiled into a `CompiledSchema`;
|
|
327
328
|
- the logger is created and starts using this schema to validate `extra`
|
|
328
329
|
fields.
|
|
330
|
+
- A valid empty schema (e.g., `{}` or a schema with only inner nodes and no
|
|
331
|
+
leaves) is treated as valid and does not cause errors. The logger is created
|
|
332
|
+
successfully, but no `extra` fields will be included in log records.
|
|
329
333
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
being wrapped in `SchemaValidationError`. Specifically:
|
|
335
|
-
|
|
336
|
-
- `OSError` when the current working directory is inaccessible or deleted (e.g.,
|
|
337
|
-
when `os.getcwd()` fails) is propagated as-is to indicate environmental issues.
|
|
338
|
-
However, `OSError` that occurs when reading the schema file (e.g., permission
|
|
339
|
-
denied, I/O errors) is converted to `SchemaValidationError` and reported as a
|
|
340
|
-
schema problem.
|
|
341
|
-
- `ValueError` when path resolution fails due to invalid characters or malformed
|
|
342
|
-
paths during schema file discovery is propagated as-is.
|
|
343
|
-
- `RuntimeError` when thread lock acquisition fails is propagated as-is.
|
|
344
|
-
|
|
345
|
-
Applications should handle these exceptions separately if they need to
|
|
346
|
-
distinguish between schema validation failures and system-level errors.
|
|
334
|
+
**Note**: System-level errors (OSError, ValueError, RuntimeError) that occur
|
|
335
|
+
during schema compilation are converted to `SchemaProblem` instances and
|
|
336
|
+
handled the same way as schema validation problems - the application is
|
|
337
|
+
terminated after logging the error to stderr.
|
|
347
338
|
|
|
348
339
|
## Schema caching and thread safety
|
|
349
340
|
|
|
@@ -395,8 +386,9 @@ distinguish between schema validation failures and system-level errors.
|
|
|
395
386
|
**it is simply not included in the final log record**.
|
|
396
387
|
|
|
397
388
|
In all of these cases a `DataProblem` is recorded for each offending field, and
|
|
398
|
-
if at least one problem is present a single
|
|
399
|
-
**after** the log record has been emitted.
|
|
389
|
+
if at least one problem is present, a single ERROR message is logged
|
|
390
|
+
**after** the log record has been emitted. The error message format is:
|
|
391
|
+
`"Log data does not match schema: {problem1}; {problem2}; ..."`.
|
|
400
392
|
|
|
401
393
|
- When a `source` is used in multiple leaves (see "Multiple leaves with the same
|
|
402
394
|
source" above), the value is validated and written independently for each leaf
|
|
@@ -412,18 +404,13 @@ High-level algorithm inside `SchemaLogger`:
|
|
|
412
404
|
2. A new structured payload is built from the schema and the given `extra`.
|
|
413
405
|
3. Only this structured payload is passed to the underlying stdlib logger.
|
|
414
406
|
4. After logging, if any validation problems were detected, a single
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
-
|
|
424
|
-
|
|
425
|
-
- the runtime type does not match the expected type;
|
|
426
|
-
- a list contains non-primitive elements (only str, int, float, bool allowed);
|
|
427
|
-
- the field is redundant and not described in the schema.
|
|
428
|
-
- Raised **after** the valid part of the payload has been logged.
|
|
429
|
-
- Contains a `problems` list describing the offending fields.
|
|
407
|
+
ERROR message is logged with the format:
|
|
408
|
+
`"Log data does not match schema: {problem1}; {problem2}; ..."`
|
|
409
|
+
(no exception is raised, ensuring 100% compatibility with standard logger behavior).
|
|
410
|
+
|
|
411
|
+
## Error handling
|
|
412
|
+
|
|
413
|
+
- Schema problems are handled internally: errors are logged to stderr and
|
|
414
|
+
the application is terminated via `os._exit(1)`.
|
|
415
|
+
- No exceptions are raised by `SchemaLogger` during initialization, making
|
|
416
|
+
it a true drop-in replacement for `logging.Logger`.
|
|
@@ -7,11 +7,8 @@ according to an application-defined JSON schema.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from .errors import DataValidationError, SchemaValidationError
|
|
11
10
|
from .schema_logger import SchemaLogger
|
|
12
11
|
|
|
13
12
|
__all__ = [
|
|
14
13
|
"SchemaLogger",
|
|
15
|
-
"SchemaValidationError",
|
|
16
|
-
"DataValidationError",
|
|
17
14
|
]
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
Attributes:
|
|
13
|
+
message: Human-readable description of the problem.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
message: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DataProblem:
|
|
21
|
+
"""Describes a single problem encountered while validating log data.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
message: Human-readable description of the validation problem.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
message: str
|
|
@@ -7,7 +7,8 @@ to user-provided extra fields, used by SchemaLogger.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from collections import defaultdict
|
|
10
|
-
from
|
|
10
|
+
from collections.abc import Mapping, MutableMapping
|
|
11
|
+
from typing import Any
|
|
11
12
|
|
|
12
13
|
from .errors import DataProblem
|
|
13
14
|
from .schema_loader import CompiledSchema, SchemaLeaf
|
|
@@ -34,8 +35,7 @@ def _validate_list_value(
|
|
|
34
35
|
"""
|
|
35
36
|
if item_expected_type is None:
|
|
36
37
|
return DataProblem(
|
|
37
|
-
f"Field '{source}' is
|
|
38
|
-
f"has no item type configured",
|
|
38
|
+
f"Field '{source}' is list but has no item type configured",
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
if len(value) == 0:
|
|
@@ -195,8 +195,8 @@ def _apply_schema_internal(
|
|
|
195
195
|
where the type matches; mismatched locations produce ``DataProblem``
|
|
196
196
|
entries, but do not affect successful locations.
|
|
197
197
|
|
|
198
|
-
The function itself does not raise
|
|
199
|
-
|
|
198
|
+
The function itself does not raise exceptions; it only accumulates
|
|
199
|
+
:class:`DataProblem` instances for the caller to handle.
|
|
200
200
|
|
|
201
201
|
Note:
|
|
202
202
|
This function is used internally by :class:`SchemaLogger` and is not
|
|
@@ -227,7 +227,7 @@ def _apply_schema_internal(
|
|
|
227
227
|
if value is None:
|
|
228
228
|
problems.append(
|
|
229
229
|
DataProblem(
|
|
230
|
-
f"Field '{source}' is None
|
|
230
|
+
f"Field '{source}' is None",
|
|
231
231
|
),
|
|
232
232
|
)
|
|
233
233
|
continue
|
|
@@ -246,7 +246,7 @@ def _apply_schema_internal(
|
|
|
246
246
|
for key in redundant_keys:
|
|
247
247
|
problems.append(
|
|
248
248
|
DataProblem(
|
|
249
|
-
f"Field '{key}' is not defined in schema
|
|
249
|
+
f"Field '{key}' is not defined in schema",
|
|
250
250
|
),
|
|
251
251
|
)
|
|
252
252
|
|
|
@@ -13,9 +13,10 @@ import json
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import threading
|
|
16
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
16
17
|
from dataclasses import dataclass
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
19
|
+
from typing import Any
|
|
19
20
|
|
|
20
21
|
from .errors import SchemaProblem
|
|
21
22
|
|
|
@@ -94,7 +95,11 @@ _resolved_schema_path: Path | None = None
|
|
|
94
95
|
# Non-None means the cached path is an absolute path based on CWD
|
|
95
96
|
# when file was not found
|
|
96
97
|
_cached_cwd: Path | None = None
|
|
97
|
-
|
|
98
|
+
# RLock for thread-safe access to path cache variables.
|
|
99
|
+
# RLock is used to allow recursive locking when helper functions
|
|
100
|
+
# (_check_cached_found_file_path, _check_cached_missing_file_path) are called
|
|
101
|
+
# from _get_schema_path() which already holds the lock.
|
|
102
|
+
_path_cache_lock = threading.RLock()
|
|
98
103
|
|
|
99
104
|
|
|
100
105
|
def _find_schema_file() -> Path | None:
|
|
@@ -144,17 +149,18 @@ def _check_cached_found_file_path() -> Path | None:
|
|
|
144
149
|
"""
|
|
145
150
|
global _resolved_schema_path
|
|
146
151
|
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
with _path_cache_lock:
|
|
153
|
+
if _resolved_schema_path is None:
|
|
154
|
+
return None
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
# Schema file was found, absolute path doesn't depend on CWD
|
|
157
|
+
# Return it if it still exists
|
|
158
|
+
if _resolved_schema_path.exists():
|
|
159
|
+
return _resolved_schema_path
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
# Cached file no longer exists; invalidate cache so caller will re-search
|
|
162
|
+
_resolved_schema_path = None
|
|
163
|
+
return None
|
|
158
164
|
|
|
159
165
|
|
|
160
166
|
def _check_cached_missing_file_path() -> Path | None:
|
|
@@ -168,20 +174,21 @@ def _check_cached_missing_file_path() -> Path | None:
|
|
|
168
174
|
"""
|
|
169
175
|
global _resolved_schema_path, _cached_cwd
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
with _path_cache_lock:
|
|
178
|
+
if _resolved_schema_path is None or _cached_cwd is None:
|
|
179
|
+
return None
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
# Cached path is based on CWD when file was not found,
|
|
182
|
+
# check if CWD changed
|
|
183
|
+
current_cwd = _get_current_working_directory()
|
|
184
|
+
if current_cwd != _cached_cwd:
|
|
185
|
+
# CWD changed, invalidate cache and re-search from new CWD
|
|
186
|
+
_resolved_schema_path = None
|
|
187
|
+
_cached_cwd = None
|
|
188
|
+
return None
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
# CWD unchanged, return cached path
|
|
191
|
+
return _resolved_schema_path
|
|
185
192
|
|
|
186
193
|
|
|
187
194
|
def _cache_and_return_found_path(found_path: Path) -> Path:
|
|
@@ -444,7 +451,8 @@ def _compile_schema_tree(
|
|
|
444
451
|
Yields:
|
|
445
452
|
SchemaLeaf objects found in the tree.
|
|
446
453
|
"""
|
|
447
|
-
# Check for excessive nesting depth
|
|
454
|
+
# Check for excessive nesting depth (DoS protection: prevent deeply nested
|
|
455
|
+
# schemas that could cause stack overflow or excessive memory usage)
|
|
448
456
|
if len(path) > MAX_SCHEMA_DEPTH:
|
|
449
457
|
problems.append(
|
|
450
458
|
SchemaProblem(
|
|
@@ -559,10 +567,9 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
|
|
|
559
567
|
to the schema, the application must restart the process. See the README
|
|
560
568
|
section \"Schema caching and thread safety\" for more details.
|
|
561
569
|
|
|
562
|
-
This function never raises
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
or invalid).
|
|
570
|
+
This function never raises exceptions. It always returns the best-effort
|
|
571
|
+
compiled schema together with a list of problems detected during processing
|
|
572
|
+
(an empty ``CompiledSchema`` when the schema is missing or invalid).
|
|
566
573
|
|
|
567
574
|
Note:
|
|
568
575
|
This function is used internally by :class:`SchemaLogger` and by the
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Logger subclass that applies a JSON schema to extra fields.
|
|
2
|
+
|
|
3
|
+
This class extends the standard ``logging.Logger`` to validate and filter
|
|
4
|
+
user-provided ``extra`` fields according to a compiled JSON schema. When
|
|
5
|
+
validation problems are detected, they are logged as ERROR messages *after*
|
|
6
|
+
the log record has been emitted, ensuring 100% compatibility with standard
|
|
7
|
+
logger behavior.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .errors import SchemaProblem
|
|
20
|
+
from .schema_applier import _apply_schema_internal
|
|
21
|
+
from .schema_loader import CompiledSchema, _compile_schema_internal
|
|
22
|
+
|
|
23
|
+
# Python 3.11+ has improved findCaller() implementation with proper stacklevel support.
|
|
24
|
+
# For Python < 3.11, we use inspect.stack() as a fallback due to known issues with
|
|
25
|
+
# findCaller() and stacklevel parameter.
|
|
26
|
+
_USE_FINDCALLER = sys.version_info >= (3, 11)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _log_schema_problems_and_exit(problems: list[SchemaProblem]) -> None:
|
|
30
|
+
"""Log schema problems to stderr and terminate the application.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
problems: List of schema problems to log.
|
|
34
|
+
"""
|
|
35
|
+
# Format error message with details of all problems
|
|
36
|
+
# (same format as data problems)
|
|
37
|
+
problem_messages = [problem.message for problem in problems]
|
|
38
|
+
error_msg = f"Schema has problems: {'; '.join(problem_messages)}\n"
|
|
39
|
+
sys.stderr.write(error_msg)
|
|
40
|
+
sys.stderr.flush()
|
|
41
|
+
os._exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SchemaLogger(logging.Logger):
|
|
45
|
+
"""Logger subclass that enforces schema on extra fields.
|
|
46
|
+
|
|
47
|
+
This class extends :class:`logging.Logger` to add validation for
|
|
48
|
+
``extra`` fields according to a compiled JSON schema. It is designed
|
|
49
|
+
to be used as a drop-in replacement via :func:`logging.setLoggerClass`.
|
|
50
|
+
|
|
51
|
+
The schema is loaded from ``logging_objects_with_schema.json`` in the
|
|
52
|
+
application root directory during initialization. If the schema cannot
|
|
53
|
+
be loaded or validated, the logger instance is not created, schema
|
|
54
|
+
problems are logged to stderr, and the application is terminated.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> import logging
|
|
58
|
+
>>> from logging_objects_with_schema import SchemaLogger
|
|
59
|
+
>>> logging.setLoggerClass(SchemaLogger)
|
|
60
|
+
>>> logger = logging.getLogger("my_service")
|
|
61
|
+
>>> logger.info("request processed", extra={"request_id": "abc-123"})
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, name: str, level: int = logging.NOTSET) -> None:
|
|
65
|
+
"""Initialise the schema-aware logger.
|
|
66
|
+
|
|
67
|
+
The schema is compiled once during construction. If any
|
|
68
|
+
problems are detected in the schema, the logger instance is not
|
|
69
|
+
created, schema problems are logged to stderr, and the application
|
|
70
|
+
is terminated.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Logger name (same as :class:`logging.Logger`).
|
|
74
|
+
level: Logger level (same as :class:`logging.Logger`).
|
|
75
|
+
"""
|
|
76
|
+
# Validate schema before creating the logger instance to avoid
|
|
77
|
+
# registering a broken logger in the logging manager cache.
|
|
78
|
+
# Schema is compiled and cached first, then problems are checked.
|
|
79
|
+
try:
|
|
80
|
+
compiled, problems = _compile_schema_internal()
|
|
81
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
82
|
+
# Convert system-level exceptions to SchemaProblem so they can be
|
|
83
|
+
# handled the same way as schema validation problems.
|
|
84
|
+
# - OSError: system-level file system issues (e.g., os.getcwd() failures
|
|
85
|
+
# when the current working directory is inaccessible or deleted).
|
|
86
|
+
# Note: OSError that occurs when reading the schema file (e.g., permission
|
|
87
|
+
# denied, I/O errors) is converted to SchemaProblem in _load_raw_schema()
|
|
88
|
+
# and does not reach this exception handler.
|
|
89
|
+
# - ValueError: path resolution issues (e.g., invalid path characters,
|
|
90
|
+
# malformed paths during schema file discovery)
|
|
91
|
+
# - RuntimeError: threading issues (e.g., lock acquisition problems)
|
|
92
|
+
# Note: JSON parsing and schema structure validation errors are
|
|
93
|
+
# converted to SchemaProblem instances and do not raise ValueError here.
|
|
94
|
+
# Note: System exceptions (KeyboardInterrupt, SystemExit) are not
|
|
95
|
+
# caught, which is the correct behavior.
|
|
96
|
+
problems = [SchemaProblem(f"Schema compilation failed: {exc}")]
|
|
97
|
+
compiled = CompiledSchema(leaves=[])
|
|
98
|
+
|
|
99
|
+
if problems:
|
|
100
|
+
# Schema is invalid; log problems and terminate without creating
|
|
101
|
+
# the logger instance.
|
|
102
|
+
_log_schema_problems_and_exit(problems)
|
|
103
|
+
|
|
104
|
+
# Schema is valid; create the logger instance.
|
|
105
|
+
super().__init__(name, level)
|
|
106
|
+
self._schema: CompiledSchema = compiled
|
|
107
|
+
|
|
108
|
+
def _log(
|
|
109
|
+
self,
|
|
110
|
+
level: int,
|
|
111
|
+
msg: object,
|
|
112
|
+
args: tuple[object, ...] | Mapping[str, object],
|
|
113
|
+
exc_info: Any = None,
|
|
114
|
+
extra: Mapping[str, object] | None = None,
|
|
115
|
+
stack_info: bool = False,
|
|
116
|
+
stacklevel: int = 1,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Log a message with the specified level and schema-validated extra.
|
|
119
|
+
|
|
120
|
+
This method validates and filters the ``extra`` parameter according
|
|
121
|
+
to the compiled schema before delegating to the parent class. If
|
|
122
|
+
validation problems are detected, they are logged as ERROR messages
|
|
123
|
+
after the main log record has been emitted, ensuring compatibility
|
|
124
|
+
with standard logger behavior (no exceptions are raised).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
level: Logging level.
|
|
128
|
+
msg: Message format string.
|
|
129
|
+
args: Arguments for message formatting.
|
|
130
|
+
exc_info: Exception information.
|
|
131
|
+
extra: Extra fields to include in the log record.
|
|
132
|
+
stack_info: Whether to include stack information.
|
|
133
|
+
stacklevel: Stack level for caller information.
|
|
134
|
+
"""
|
|
135
|
+
structured_extra, data_problems = _apply_schema_internal(
|
|
136
|
+
self._schema,
|
|
137
|
+
extra or {},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
super()._log(
|
|
141
|
+
level,
|
|
142
|
+
msg,
|
|
143
|
+
args,
|
|
144
|
+
exc_info=exc_info,
|
|
145
|
+
extra=structured_extra,
|
|
146
|
+
stack_info=stack_info,
|
|
147
|
+
# Increment stacklevel to account for this override frame so that
|
|
148
|
+
# caller information points to user code instead of SchemaLogger._log.
|
|
149
|
+
stacklevel=stacklevel + 1,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if data_problems:
|
|
153
|
+
# Log validation errors as ERROR messages
|
|
154
|
+
# Get caller information for the error log record.
|
|
155
|
+
# Python 3.11+ has improved findCaller() with proper stacklevel support,
|
|
156
|
+
# so we use it as the primary method for better performance.
|
|
157
|
+
# For Python < 3.11, we fall back to inspect.stack() due to known issues
|
|
158
|
+
# with findCaller() and stacklevel parameter.
|
|
159
|
+
if _USE_FINDCALLER:
|
|
160
|
+
# Use findCaller() for Python 3.11+ (more efficient)
|
|
161
|
+
fn, lno, func, sinfo = self.findCaller(
|
|
162
|
+
stack_info=False, stacklevel=stacklevel + 1
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
# Fallback to inspect.stack() for Python < 3.11
|
|
166
|
+
# The stack looks like:
|
|
167
|
+
# - Frame 0: this function (_log)
|
|
168
|
+
# - Frame 1: logger.info() wrapper
|
|
169
|
+
# - Frame 2: actual caller (test_function)
|
|
170
|
+
# We need to skip frame 0 (this function) and frame 1
|
|
171
|
+
# (logger.info wrapper), so we use stacklevel + 1 to get to
|
|
172
|
+
# the actual caller.
|
|
173
|
+
stack = inspect.stack()
|
|
174
|
+
frame_idx = (
|
|
175
|
+
stacklevel + 1
|
|
176
|
+
) # Skip this function (0) + logger.info wrapper (1)
|
|
177
|
+
if frame_idx < len(stack):
|
|
178
|
+
frame = stack[frame_idx]
|
|
179
|
+
fn = frame.filename
|
|
180
|
+
lno = frame.lineno
|
|
181
|
+
func = frame.function
|
|
182
|
+
sinfo = None
|
|
183
|
+
else:
|
|
184
|
+
# Fallback to findCaller if stack is shorter than expected
|
|
185
|
+
fn, lno, func, sinfo = self.findCaller(
|
|
186
|
+
stack_info=False, stacklevel=stacklevel + 1
|
|
187
|
+
)
|
|
188
|
+
# Format error message with details of all problems
|
|
189
|
+
problem_messages = [problem.message for problem in data_problems]
|
|
190
|
+
error_msg = f"Log data does not match schema: {'; '.join(problem_messages)}"
|
|
191
|
+
error_record = self.makeRecord(
|
|
192
|
+
self.name,
|
|
193
|
+
logging.ERROR,
|
|
194
|
+
fn,
|
|
195
|
+
lno,
|
|
196
|
+
error_msg,
|
|
197
|
+
(),
|
|
198
|
+
None, # exc_info - not needed
|
|
199
|
+
func, # func - function name from caller
|
|
200
|
+
None, # extra - not needed
|
|
201
|
+
sinfo, # sinfo - stack info from caller
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
self.callHandlers(error_record)
|
|
205
|
+
except Exception:
|
|
206
|
+
# If handler failed, log error to stderr (standard logging behavior)
|
|
207
|
+
sys.stderr.write(f"Error in logging handler: {error_record}\n")
|
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
Attributes:
|
|
13
|
-
message: Human-readable description of the problem.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
message: str
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SchemaValidationError(Exception):
|
|
20
|
-
"""Raised when there are problems with the JSON schema definition.
|
|
21
|
-
|
|
22
|
-
This exception is raised during SchemaLogger initialization when the schema
|
|
23
|
-
file cannot be loaded, parsed, or validated. The logger instance will not
|
|
24
|
-
be created if this exception is raised.
|
|
25
|
-
|
|
26
|
-
The human-readable summary is stored in the exception message, while
|
|
27
|
-
detailed information about each violation is available in the ``problems``
|
|
28
|
-
attribute.
|
|
29
|
-
|
|
30
|
-
Attributes:
|
|
31
|
-
problems: List of SchemaProblem instances describing each validation issue.
|
|
32
|
-
|
|
33
|
-
Example:
|
|
34
|
-
>>> try:
|
|
35
|
-
... logger = SchemaLogger("my_logger")
|
|
36
|
-
... except SchemaValidationError as e:
|
|
37
|
-
... for problem in e.problems:
|
|
38
|
-
... print(f"Schema error: {problem.message}")
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self, message: str, problems: list[SchemaProblem] | None = None
|
|
43
|
-
) -> None:
|
|
44
|
-
super().__init__(message)
|
|
45
|
-
self.problems: list[SchemaProblem] = problems or []
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class DataProblem:
|
|
50
|
-
"""Describes a single problem encountered while validating log data.
|
|
51
|
-
|
|
52
|
-
Attributes:
|
|
53
|
-
message: Human-readable description of the validation problem.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
message: str
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class DataValidationError(Exception):
|
|
60
|
-
"""Raised when log record data does not satisfy the configured schema.
|
|
61
|
-
|
|
62
|
-
This exception is raised *after* the valid part of the log record
|
|
63
|
-
has already been formatted and sent to the underlying handler. This means
|
|
64
|
-
that even if validation fails, the valid fields will still appear in the log.
|
|
65
|
-
|
|
66
|
-
The exception message provides a summary description of the validation
|
|
67
|
-
failure, while detailed information about individual problems is exposed
|
|
68
|
-
via the ``problems`` attribute.
|
|
69
|
-
|
|
70
|
-
Attributes:
|
|
71
|
-
problems: List of DataProblem instances describing each validation issue.
|
|
72
|
-
|
|
73
|
-
Example:
|
|
74
|
-
>>> try:
|
|
75
|
-
... logger.info("processing", extra={"user_id": "not-an-int"})
|
|
76
|
-
... except DataValidationError as e:
|
|
77
|
-
... for problem in e.problems:
|
|
78
|
-
... print(f"Validation error: {problem.message}")
|
|
79
|
-
... # Note: valid fields were already logged before this exception
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
def __init__(self, message: str, problems: list[DataProblem] | None = None) -> None:
|
|
83
|
-
super().__init__(message)
|
|
84
|
-
self.problems: list[DataProblem] = problems or []
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
"""Logger subclass that applies a JSON schema to extra fields.
|
|
2
|
-
|
|
3
|
-
This class extends the standard ``logging.Logger`` to validate and filter
|
|
4
|
-
user-provided ``extra`` fields according to a compiled JSON schema. It raises
|
|
5
|
-
a DataValidationError *after* the log record has been emitted when problems
|
|
6
|
-
are detected.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import logging
|
|
12
|
-
from collections.abc import Mapping
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
from .errors import DataValidationError, SchemaValidationError
|
|
16
|
-
from .schema_applier import _apply_schema_internal
|
|
17
|
-
from .schema_loader import CompiledSchema, _compile_schema_internal
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SchemaLogger(logging.Logger):
|
|
21
|
-
"""Logger subclass that enforces schema on extra fields.
|
|
22
|
-
|
|
23
|
-
This class extends :class:`logging.Logger` to add validation for
|
|
24
|
-
``extra`` fields according to a compiled JSON schema. It is designed
|
|
25
|
-
to be used as a drop-in replacement via :func:`logging.setLoggerClass`.
|
|
26
|
-
|
|
27
|
-
The schema is loaded from ``logging_objects_with_schema.json`` in the
|
|
28
|
-
application root directory during initialization. If the schema cannot
|
|
29
|
-
be loaded or validated, a :class:`SchemaValidationError` is raised.
|
|
30
|
-
|
|
31
|
-
Example:
|
|
32
|
-
>>> import logging
|
|
33
|
-
>>> from logging_objects_with_schema import SchemaLogger
|
|
34
|
-
>>> logging.setLoggerClass(SchemaLogger)
|
|
35
|
-
>>> logger = logging.getLogger("my_service")
|
|
36
|
-
>>> logger.info("request processed", extra={"request_id": "abc-123"})
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, name: str, level: int = logging.NOTSET) -> None:
|
|
40
|
-
"""Initialise the schema-aware logger.
|
|
41
|
-
|
|
42
|
-
The schema is compiled once during construction. If any
|
|
43
|
-
problems are detected in the schema, a SchemaValidationError
|
|
44
|
-
will be raised and this logger instance is not usable.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
name: Logger name (same as :class:`logging.Logger`).
|
|
48
|
-
level: Logger level (same as :class:`logging.Logger`).
|
|
49
|
-
"""
|
|
50
|
-
super().__init__(name, level)
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
compiled, problems = _compile_schema_internal()
|
|
54
|
-
except (OSError, ValueError, RuntimeError):
|
|
55
|
-
# Catch specific exceptions that can occur during schema compilation:
|
|
56
|
-
# - OSError: system-level file system issues (e.g., os.getcwd() failures
|
|
57
|
-
# when the current working directory is inaccessible or deleted).
|
|
58
|
-
# Note: OSError that occurs when reading the schema file (e.g., permission
|
|
59
|
-
# denied, I/O errors) is converted to SchemaValidationError in
|
|
60
|
-
# _load_raw_schema() and does not reach this exception handler.
|
|
61
|
-
# - ValueError: path resolution issues (e.g., invalid path characters,
|
|
62
|
-
# malformed paths during schema file discovery)
|
|
63
|
-
# - RuntimeError: threading issues (e.g., lock acquisition problems)
|
|
64
|
-
# Note: JSON parsing and schema structure validation errors are
|
|
65
|
-
# converted to SchemaProblem instances and do not raise ValueError here.
|
|
66
|
-
# Note: System exceptions (KeyboardInterrupt, SystemExit) are not
|
|
67
|
-
# caught, which is the correct behavior.
|
|
68
|
-
# Ensure that a partially initialised logger instance is not left
|
|
69
|
-
# registered in the logging manager if schema compilation fails.
|
|
70
|
-
# Otherwise, subsequent logging.getLogger(name) calls could return
|
|
71
|
-
# this broken instance and lead to AttributeError at runtime.
|
|
72
|
-
self._cleanup_failed_logger()
|
|
73
|
-
raise
|
|
74
|
-
|
|
75
|
-
if problems:
|
|
76
|
-
# Schema is invalid; remove this instance from the logging manager
|
|
77
|
-
# cache before raising, for the same reason as above.
|
|
78
|
-
self._cleanup_failed_logger()
|
|
79
|
-
raise SchemaValidationError("Schema has problems", problems=problems)
|
|
80
|
-
|
|
81
|
-
self._schema: CompiledSchema = compiled
|
|
82
|
-
|
|
83
|
-
def _cleanup_failed_logger(self) -> None:
|
|
84
|
-
"""Remove this logger instance from the logging manager.
|
|
85
|
-
|
|
86
|
-
Called when schema compilation fails to prevent broken logger
|
|
87
|
-
instances from being cached and reused.
|
|
88
|
-
"""
|
|
89
|
-
self.manager.loggerDict.pop(self.name, None)
|
|
90
|
-
|
|
91
|
-
def _log(
|
|
92
|
-
self,
|
|
93
|
-
level: int,
|
|
94
|
-
msg: object,
|
|
95
|
-
args: tuple[object, ...] | Mapping[str, object],
|
|
96
|
-
exc_info: Any = None,
|
|
97
|
-
extra: Mapping[str, object] | None = None,
|
|
98
|
-
stack_info: bool = False,
|
|
99
|
-
stacklevel: int = 1,
|
|
100
|
-
) -> None:
|
|
101
|
-
"""Log a message with the specified level and schema-validated extra.
|
|
102
|
-
|
|
103
|
-
This method validates and filters the ``extra`` parameter according
|
|
104
|
-
to the compiled schema before delegating to the parent class.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
level: Logging level.
|
|
108
|
-
msg: Message format string.
|
|
109
|
-
args: Arguments for message formatting.
|
|
110
|
-
exc_info: Exception information.
|
|
111
|
-
extra: Extra fields to include in the log record.
|
|
112
|
-
stack_info: Whether to include stack information.
|
|
113
|
-
stacklevel: Stack level for caller information.
|
|
114
|
-
|
|
115
|
-
Raises:
|
|
116
|
-
DataValidationError: If the ``extra`` data does not match the schema.
|
|
117
|
-
"""
|
|
118
|
-
structured_extra, data_problems = _apply_schema_internal(
|
|
119
|
-
self._schema,
|
|
120
|
-
extra or {},
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
super()._log(
|
|
124
|
-
level,
|
|
125
|
-
msg,
|
|
126
|
-
args,
|
|
127
|
-
exc_info=exc_info,
|
|
128
|
-
extra=structured_extra,
|
|
129
|
-
stack_info=stack_info,
|
|
130
|
-
# Increment stacklevel to account for this override frame so that
|
|
131
|
-
# caller information points to user code instead of SchemaLogger._log.
|
|
132
|
-
stacklevel=stacklevel + 1,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if data_problems:
|
|
136
|
-
raise DataValidationError(
|
|
137
|
-
"Log data does not match schema",
|
|
138
|
-
problems=data_problems,
|
|
139
|
-
)
|
|
File without changes
|