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.
@@ -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`.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "logging-objects-with-schema"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Proxy logging wrapper that validates extra fields against a JSON schema."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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