logging-objects-with-schema 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logging_objects_with_schema/__init__.py +17 -0
- logging_objects_with_schema/errors.py +85 -0
- logging_objects_with_schema/schema_applier.py +184 -0
- logging_objects_with_schema/schema_loader.py +474 -0
- logging_objects_with_schema/schema_logger.py +118 -0
- logging_objects_with_schema-0.1.0.dist-info/METADATA +440 -0
- logging_objects_with_schema-0.1.0.dist-info/RECORD +9 -0
- logging_objects_with_schema-0.1.0.dist-info/WHEEL +4 -0
- logging_objects_with_schema-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Top-level package for logging_objects_with_schema.
|
|
2
|
+
|
|
3
|
+
This package provides a logger subclass built on top of the standard
|
|
4
|
+
logging package that validates and filters user-provided extra fields
|
|
5
|
+
according to an application-defined JSON schema.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .errors import DataValidationError, SchemaValidationError
|
|
11
|
+
from .schema_logger import SchemaLogger
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SchemaLogger",
|
|
15
|
+
"SchemaValidationError",
|
|
16
|
+
"DataValidationError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Custom exception types used by logging_objects_with_schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SchemaProblem:
|
|
11
|
+
"""Describes a single problem encountered while loading the schema.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
message: Human-readable description of the problem.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
message: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SchemaValidationError(Exception):
|
|
21
|
+
"""Raised when there are problems with the JSON schema definition.
|
|
22
|
+
|
|
23
|
+
This exception is raised during SchemaLogger initialization when the schema
|
|
24
|
+
file cannot be loaded, parsed, or validated. The logger instance will not
|
|
25
|
+
be created if this exception is raised.
|
|
26
|
+
|
|
27
|
+
The human-readable summary is stored in the exception message, while
|
|
28
|
+
detailed information about each violation is available in the ``problems``
|
|
29
|
+
attribute.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
problems: List of SchemaProblem instances describing each validation issue.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> try:
|
|
36
|
+
... logger = SchemaLogger("my_logger")
|
|
37
|
+
... except SchemaValidationError as e:
|
|
38
|
+
... for problem in e.problems:
|
|
39
|
+
... print(f"Schema error: {problem.message}")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, message: str, problems: List[SchemaProblem] | None = None
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
self.problems: List[SchemaProblem] = problems or []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DataProblem:
|
|
51
|
+
"""Describes a single problem encountered while validating log data.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
message: Human-readable description of the validation problem.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
message: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DataValidationError(Exception):
|
|
61
|
+
"""Raised when log record data does not satisfy the configured schema.
|
|
62
|
+
|
|
63
|
+
This exception is raised *after* the valid part of the log record
|
|
64
|
+
has already been formatted and sent to the underlying handler. This means
|
|
65
|
+
that even if validation fails, the valid fields will still appear in the log.
|
|
66
|
+
|
|
67
|
+
The exception message provides a summary description of the validation
|
|
68
|
+
failure, while detailed information about individual problems is exposed
|
|
69
|
+
via the ``problems`` attribute.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
problems: List of DataProblem instances describing each validation issue.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> try:
|
|
76
|
+
... logger.info("processing", extra={"user_id": "not-an-int"})
|
|
77
|
+
... except DataValidationError as e:
|
|
78
|
+
... for problem in e.problems:
|
|
79
|
+
... print(f"Validation error: {problem.message}")
|
|
80
|
+
... # Note: valid fields were already logged before this exception
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, message: str, problems: List[DataProblem] | None = None) -> None:
|
|
84
|
+
super().__init__(message)
|
|
85
|
+
self.problems: List[DataProblem] = problems or []
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Internal schema application logic for logging_objects_with_schema.
|
|
2
|
+
|
|
3
|
+
This module provides the core function for applying a compiled schema
|
|
4
|
+
to user-provided extra fields, used by SchemaLogger.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Mapping, MutableMapping, Tuple
|
|
10
|
+
|
|
11
|
+
from .errors import DataProblem
|
|
12
|
+
from .schema_loader import CompiledSchema, SchemaLeaf
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_empty(node: Any) -> Any:
|
|
16
|
+
"""Remove empty dictionaries and None values from a nested structure.
|
|
17
|
+
|
|
18
|
+
Recursively walks through a dictionary structure and removes:
|
|
19
|
+
- Empty dictionaries ({})
|
|
20
|
+
- None values
|
|
21
|
+
|
|
22
|
+
This helper is used by ``_apply_schema_internal`` on the final payload
|
|
23
|
+
to avoid leaving empty containers created during schema application.
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
This function is part of the internal implementation details and is
|
|
27
|
+
not considered a public API. Its signature and behaviour may change
|
|
28
|
+
between releases without preserving backward compatibility.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
node: The node to process (can be dict, list, or any other type).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The cleaned structure with empty dicts and None values removed.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(node, dict):
|
|
37
|
+
cleaned = {k: _strip_empty(v) for k, v in node.items()}
|
|
38
|
+
return {k: v for k, v in cleaned.items() if v != {} and v is not None}
|
|
39
|
+
return node
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _apply_schema_internal(
|
|
43
|
+
compiled: CompiledSchema,
|
|
44
|
+
extra_values: Mapping[str, Any],
|
|
45
|
+
) -> Tuple[Dict[str, Any], List[DataProblem]]:
|
|
46
|
+
"""Internal function to build structured ``extra`` from compiled schema.
|
|
47
|
+
|
|
48
|
+
The function applies a :class:`CompiledSchema` to user-provided ``extra``
|
|
49
|
+
values and returns a tuple ``(structured_extra, problems)`` where:
|
|
50
|
+
|
|
51
|
+
- ``structured_extra`` is a nested dictionary that follows the schema
|
|
52
|
+
structure and contains only fields that passed validation;
|
|
53
|
+
- ``problems`` is a list of :class:`DataProblem` describing all data
|
|
54
|
+
issues observed during processing.
|
|
55
|
+
|
|
56
|
+
Behaviour summary:
|
|
57
|
+
|
|
58
|
+
- If the compiled schema is effectively empty (no valid leaves),
|
|
59
|
+
all fields from ``extra_values`` are treated as redundant: the returned
|
|
60
|
+
payload is empty, and a :class:`DataProblem` is created for each field.
|
|
61
|
+
- For each ``source`` mentioned in the schema when there are valid leaves:
|
|
62
|
+
- if the source is missing from ``extra_values``, it is silently skipped;
|
|
63
|
+
- if the corresponding value is ``None``, a ``DataProblem`` is recorded
|
|
64
|
+
and the value is not written to the payload.
|
|
65
|
+
- Type checks are strict: the runtime type must exactly match the declared
|
|
66
|
+
Python type (``type(value) is leaf.expected_type``). This prevents
|
|
67
|
+
``bool`` from being accepted where ``int`` is expected.
|
|
68
|
+
- For list-typed leaves:
|
|
69
|
+
- empty lists are accepted;
|
|
70
|
+
- all elements must have the exact type declared by the leaf
|
|
71
|
+
``item_expected_type`` (for example, list[str], list[int]);
|
|
72
|
+
- non-primitive elements and elements of a different primitive type are
|
|
73
|
+
rejected with a ``DataProblem`` and the list value is not written.
|
|
74
|
+
- Redundant fields from ``extra_values`` (not referenced by any leaf
|
|
75
|
+
``source``) are always reported as problems: each such field generates
|
|
76
|
+
a :class:`DataProblem` indicating that it is not defined in the schema.
|
|
77
|
+
- A single ``source`` may be used by multiple leaves. The value is
|
|
78
|
+
validated independently for each leaf and written only to locations
|
|
79
|
+
where the type matches; mismatched locations produce ``DataProblem``
|
|
80
|
+
entries, but do not affect successful locations.
|
|
81
|
+
|
|
82
|
+
The function itself does not raise ``DataValidationError``; it only
|
|
83
|
+
accumulates :class:`DataProblem` instances for the caller to handle.
|
|
84
|
+
|
|
85
|
+
Note:
|
|
86
|
+
This function is used internally by :class:`SchemaLogger` and is not
|
|
87
|
+
considered part of the public API. Its signature and behaviour may
|
|
88
|
+
change between releases without preserving backward compatibility.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (structured_extra, List[DataProblem]).
|
|
92
|
+
"""
|
|
93
|
+
extra: Dict[str, Any] = {}
|
|
94
|
+
problems: List[DataProblem] = []
|
|
95
|
+
|
|
96
|
+
used_sources = {leaf.source for leaf in compiled.leaves}
|
|
97
|
+
|
|
98
|
+
source_to_leaves: Dict[str, List[SchemaLeaf]] = {}
|
|
99
|
+
for leaf in compiled.leaves:
|
|
100
|
+
source_to_leaves.setdefault(leaf.source, []).append(leaf)
|
|
101
|
+
|
|
102
|
+
for source, leaves in source_to_leaves.items():
|
|
103
|
+
if source not in extra_values:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
value = extra_values.get(source)
|
|
107
|
+
|
|
108
|
+
# Check for None values explicitly (None values are not allowed)
|
|
109
|
+
# This check must be done once per source, not once per leaf
|
|
110
|
+
if value is None:
|
|
111
|
+
problems.append(
|
|
112
|
+
DataProblem(
|
|
113
|
+
f"Field '{source}' is None, but None values are not allowed",
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
for leaf in leaves:
|
|
119
|
+
# Use strict type checking (type() is) instead of isinstance() to
|
|
120
|
+
# prevent bool values from passing validation for int types (since
|
|
121
|
+
# bool is a subclass of int). This ensures that the actual
|
|
122
|
+
# runtime type matches the schema type exactly.
|
|
123
|
+
if type(value) is not leaf.expected_type:
|
|
124
|
+
problems.append(
|
|
125
|
+
DataProblem(
|
|
126
|
+
f"Field '{source}' has type {type(value).__name__}, "
|
|
127
|
+
f"expected {leaf.expected_type.__name__}",
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# For lists, validate that all elements strictly match the declared
|
|
133
|
+
# item_expected_type (homogeneous primitive list).
|
|
134
|
+
if leaf.expected_type is list and isinstance(value, list):
|
|
135
|
+
item_expected_type = leaf.item_expected_type
|
|
136
|
+
# item_expected_type should always be set for list leaves by the
|
|
137
|
+
# schema compiler, but we guard defensively.
|
|
138
|
+
if item_expected_type is None:
|
|
139
|
+
problems.append(
|
|
140
|
+
DataProblem(
|
|
141
|
+
f"Field '{source}' is declared as list in schema but "
|
|
142
|
+
f"has no item type configured",
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if len(value) > 0:
|
|
148
|
+
invalid_item_types = {
|
|
149
|
+
type(item).__name__
|
|
150
|
+
for item in value
|
|
151
|
+
if type(item) is not item_expected_type
|
|
152
|
+
}
|
|
153
|
+
if invalid_item_types:
|
|
154
|
+
problems.append(
|
|
155
|
+
DataProblem(
|
|
156
|
+
f"Field '{source}' is a list but contains elements "
|
|
157
|
+
f"with types {sorted(invalid_item_types)}; "
|
|
158
|
+
f"expected all elements to be of type "
|
|
159
|
+
f"{item_expected_type.__name__}",
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
target: MutableMapping[str, Any] = extra
|
|
165
|
+
for key in leaf.path[:-1]:
|
|
166
|
+
child = target.get(key)
|
|
167
|
+
if not isinstance(child, dict):
|
|
168
|
+
child = {}
|
|
169
|
+
target[key] = child
|
|
170
|
+
target = child
|
|
171
|
+
|
|
172
|
+
target[leaf.path[-1]] = value
|
|
173
|
+
|
|
174
|
+
# Report redundant fields for any keys not referenced by schema leaves.
|
|
175
|
+
for key in extra_values.keys():
|
|
176
|
+
if key not in used_sources:
|
|
177
|
+
problems.append(
|
|
178
|
+
DataProblem(
|
|
179
|
+
f"Field '{key}' is not defined in schema and will be ignored",
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
cleaned_extra = _strip_empty(extra)
|
|
184
|
+
return cleaned_extra, problems
|