{"id":36991315,"url":"https://github.com/disafronov/python-logging-objects-with-schema","last_synced_at":"2026-01-13T23:42:30.742Z","repository":{"id":328065693,"uuid":"1109998574","full_name":"disafronov/python-logging-objects-with-schema","owner":"disafronov","description":"This library provides a logger subclass built on top of the standard `logging` module that strictly controls additional `extra` fields using a JSON schema.","archived":false,"fork":false,"pushed_at":"2025-12-26T03:38:38.000Z","size":327,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-27T13:58:28.503Z","etag":null,"topics":["json","logging","python","python3","schema-validation"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/logging-objects-with-schema/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/disafronov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-04T15:16:44.000Z","updated_at":"2025-12-26T03:37:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/disafronov/python-logging-objects-with-schema","commit_stats":null,"previous_names":["disafronov/python-logging-objects-with-schema"],"tags_count":24,"template":false,"template_full_name":"disafronov/semantic-basic-template","purl":"pkg:github/disafronov/python-logging-objects-with-schema","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disafronov%2Fpython-logging-objects-with-schema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disafronov%2Fpython-logging-objects-with-schema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disafronov%2Fpython-logging-objects-with-schema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disafronov%2Fpython-logging-objects-with-schema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/disafronov","download_url":"https://codeload.github.com/disafronov/python-logging-objects-with-schema/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disafronov%2Fpython-logging-objects-with-schema/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28399512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json","logging","python","python3","schema-validation"],"created_at":"2026-01-13T23:42:30.088Z","updated_at":"2026-01-13T23:42:30.736Z","avatar_url":"https://github.com/disafronov.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# python-logging-objects-with-schema\n\nThis library provides a logger subclass built on top of the standard `logging`\nmodule that strictly controls additional `extra` fields using a JSON schema.\n`SchemaLogger` is a drop-in replacement for `logging.Logger` that validates\n`extra` fields against a JSON schema file (`logging_objects_with_schema.json`)\nin your application root directory.\n\n## Schema as a contract\n\nThe JSON schema is treated as a **contract** between all parties that produce\nand consume logs in the system. The schema file (`logging_objects_with_schema.json`)\nis a shared, versioned artifact that defines which structured fields are allowed\nto appear in logs and which Python types they must have. This contract ensures\nthat all downstream consumers (search systems, alerts, dashboards, external\nsystems) can rely on a consistent log structure.\n\n**Strictness guarantees:**\n\n- **`extra` fields never go directly into logs.** They are always projected\n  through the schema: values from `extra` are taken by `source` field names\n  and placed into the log structure according to the schema paths. Only fields\n  explicitly described in the schema (as leaves with `type` and `source`) can\n  ever reach your logs. The schema is the only source of truth for which\n  `extra` fields are allowed.\n- Any `extra` field that is **not** described in the schema is treated as a\n  data error: it is dropped from the log output and recorded as a validation\n  problem.\n- Any mismatch between runtime values and the declared types (wrong types,\n  `None` values, disallowed list elements) is also treated as a data error.\n- All validation problems are aggregated and logged as a single ERROR message\n  **after** the log record has been emitted, ensuring 100% compatibility with\n  standard logger behavior (no exceptions are raised).\n- Application code must only send `extra` fields that are described in the\n  schema and match the declared Python types. Any deviation from the schema\n  is considered a contract violation.\n\n## Installation\n\n```bash\npip install logging-objects-with-schema\n```\n\n## Basic usage\n\n### Quickstart (complete working example)\n\nFirst, create a schema file `logging_objects_with_schema.json` in your application root:\n\n```json\n{\n  \"ServicePayload\": {\n    \"RequestID\": {\"type\": \"str\", \"source\": \"request_id\"},\n    \"UserID\": {\"type\": \"int\", \"source\": \"user_id\"}\n  }\n}\n```\n\nThen, set up and use the logger:\n\n```python\nimport logging\nimport sys\n\nfrom logging_objects_with_schema import SchemaLogger\n\n\n# Set SchemaLogger as the default logger class\nlogging.setLoggerClass(SchemaLogger)\n\n# Configure handlers and formatters as usual\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setFormatter(logging.Formatter(\"%(message)s %(ServicePayload)s\"))\n\n# Get loggers using standard logging API\nlogger = logging.getLogger(\"service\")\nlogger.setLevel(logging.INFO)\nlogger.addHandler(handler)\n\n# Use the logger - extra fields are validated against the schema\nlogger.info(\"request processed\", extra={\"request_id\": \"abc-123\", \"user_id\": 42})\n```\n\n### Example with nested structures\n\nSchema with nested structure:\n\n```json\n{\n  \"ServicePayload\": {\n    \"RequestID\": {\"type\": \"str\", \"source\": \"request_id\"},\n    \"UserID\": {\"type\": \"int\", \"source\": \"user_id\"},\n    \"Metrics\": {\n      \"CPU\": {\"type\": \"float\", \"source\": \"cpu_usage\"},\n      \"Memory\": {\"type\": \"float\", \"source\": \"memory_usage\"},\n      \"Network\": {\n        \"In\": {\"type\": \"int\", \"source\": \"network_in\"},\n        \"Out\": {\"type\": \"int\", \"source\": \"network_out\"}\n      }\n    }\n  }\n}\n```\n\nUsage:\n\n```python\nlogger.info(\n    \"metrics collected\",\n    extra={\n        \"request_id\": \"req-123\",\n        \"user_id\": 42,\n        \"cpu_usage\": 75.5,\n        \"memory_usage\": 60.2,\n        \"network_in\": 1024,\n        \"network_out\": 2048,\n    }\n)\n```\n\n### Error handling example\n\n```python\nfrom logging_objects_with_schema import SchemaLogger\n\n# SchemaLogger is a drop-in replacement - no exception handling needed.\n# If the schema has problems, the application will be terminated after\n# logging schema problems to stderr.\nlogging.setLoggerClass(SchemaLogger)\nlogger = logging.getLogger(\"service\")\n\n# When logging with invalid data, validation errors are automatically\n# logged as ERROR messages. No exception handling is needed.\nlogger.info(\"processing\", extra={\"user_id\": \"not-an-int\"})  # Wrong type\n# The valid part of the log is emitted, and validation errors are logged\n# as ERROR messages in JSON format: {\"validation_errors\": [{\"field\": \"...\", \"error\": \"...\", \"value\": \"...\"}]}\n```\n\n## Schema location and format\n\nThe schema file `logging_objects_with_schema.json` must be located in your\napplication root directory. The library searches upward from the current working\ndirectory until it finds the file or reaches the filesystem root.\n\n**Important**: If there are any problems with the schema (missing file, broken\nJSON, invalid structure, etc.), the application is terminated after logging\nschema problems to stderr. Schema validation happens when the first logger\ninstance is created.\n\n**Note**: The schema is compiled once per process and cached. Schema changes\nrequire an application restart to take effect. The library is thread-safe.\n\nA valid empty schema (`{}`) is allowed and will result in no `extra` fields\nbeing included in log records.\n\nAn example schema:\n\n```json\n{\n  \"ServicePayload\": {\n    \"RequestID\": {\"type\": \"str\", \"source\": \"request_id\"},\n    \"UserID\": {\"type\": \"int\", \"source\": \"user_id\"},\n    \"Metrics\": {\n      \"CPU\": {\"type\": \"float\", \"source\": \"cpu_usage\"},\n      \"Memory\": {\"type\": \"float\", \"source\": \"memory_usage\"},\n      \"Network\": {\n        \"In\": {\"type\": \"int\", \"source\": \"network_in\"},\n        \"Out\": {\"type\": \"int\", \"source\": \"network_out\"}\n      }\n    }\n  }\n}\n```\n\nAn example of a valid empty schema (no leaves, no problems):\n\n```json\n{}\n```\n\n**Schema structure:**\n\n- **Inner nodes**: Objects that contain child nodes (any fields that are objects).\n  Inner nodes cannot have `type` or `source` as strings (they can only have\n  these fields as objects, which are child nodes). Inner nodes must have at\n  least one child node.\n- **Leaf nodes**: Objects with `type` and `source` fields as strings (properties).\n  A valid leaf must have both `type` and `source` present and non-empty. Leaf\n  nodes cannot have child nodes (any fields that are objects).\n- **Node validation rules**:\n  - A node cannot be both a leaf and an inner node (cannot have both properties\n    and children simultaneously).\n  - A node cannot be empty (must be either a leaf with properties or an inner\n    node with children).\n  - Fields `type`, `source`, and `item_type` can be used as child node names\n    in inner nodes (as objects), but then they are treated as children, not\n    as leaf properties.\n- **`type`**: One of `\"str\"`, `\"int\"`, `\"float\"`, `\"bool\"`, or `\"list\"`.\n  Must be a string for leaf nodes.\n- **`source`**: The name of the field in `extra` from which the value is taken.\n  Must be a string for leaf nodes.\n- **`item_type`**: Optional field for list-typed leaves. Must be a string if\n  present. See \"List-typed fields\" section below.\n- **Root key restrictions**: Root keys cannot conflict with standard `logging`\n  module fields (e.g., `name`, `levelno`, `pathname`). Such conflicts cause\n  schema validation to fail.\n\n**List-typed fields:**\n\nFor `\"type\": \"list\"`, you must also specify `\"item_type\"` (one of `\"str\"`,\n`\"int\"`, `\"float\"`, `\"bool\"`). Lists must contain homogeneous primitive elements\nof the declared type. Empty lists are allowed; nested lists and dictionaries are\nnot permitted.\n\nExample of a valid list field:\n\n```json\n{\n  \"ServicePayload\": {\n    \"Tags\": {\n      \"type\": \"list\",\n      \"source\": \"tags\",\n      \"item_type\": \"str\"\n    }\n  }\n}\n```\n\nUsage:\n\n```python\nlogger.info(\n    \"request processed\",\n    extra={\n        \"tags\": [\"blue\", \"fast\", \"cached\"],  # list[str] – valid\n    },\n)\n```\n\nInvalid example (non-primitive elements are rejected):\n\n```python\nlogger.info(\"request processed\", extra={\"tags\": [{\"key\": \"color\"}]})  # Invalid\n# Validation error is logged as ERROR after the log record is emitted\n```\n\n**Multiple leaves with the same source:**\n\nA single `source` field name can be used in multiple leaves. The value is\nvalidated independently for each leaf and written to all matching locations.\nIf types conflict, the value is written only where the type matches, and\nvalidation errors are reported for mismatched locations.\n\nExample:\n\n```json\n{\n  \"ServicePayload\": {\n    \"RequestID\": {\"type\": \"str\", \"source\": \"request_id\"},\n    \"Metadata\": {\n      \"ID\": {\"type\": \"str\", \"source\": \"request_id\"}\n    }\n  }\n}\n```\n\nWith `extra={\"request_id\": \"abc-123\"}`, the value appears in both\n`ServicePayload.RequestID` and `ServicePayload.Metadata.ID`.\n\n## Inheritance and custom forbidden root keys\n\n`SchemaLogger` supports inheritance, allowing subclasses to add additional\nforbidden root keys for schema validation. This is useful when you need to\nprevent certain root keys from being used in your schema beyond the builtin\n`logging.LogRecord` attributes.\n\n### Basic inheritance\n\nEach subclass can pass the `forbidden_keys` parameter to the parent's\n`__init__()` method. The builtin set of forbidden keys (standard `logging.LogRecord`\nattributes) is always present and cannot be replaced - additional keys are\nmerged with the builtin set.\n\nExample:\n\n```python\nfrom logging_objects_with_schema import SchemaLogger\nimport logging\n\nclass MyLogger(SchemaLogger):\n    def __init__(self, name: str, level: int = logging.NOTSET) -\u003e None:\n        # Add custom forbidden keys\n        super().__init__(name, level, forbidden_keys={\"custom_forbidden_key\"})\n```\n\n### Multi-level inheritance\n\nWhen creating a hierarchy of loggers, each subclass can pass `forbidden_keys`\nfrom its own subclasses to the parent, merging them with its own set. The\nlibrary does not automatically propagate keys up the inheritance chain - each\nsubclass must implement this logic itself.\n\nExample:\n\n```python\nfrom logging_objects_with_schema import SchemaLogger\nimport logging\n\nclass ParentLogger(SchemaLogger):\n    def __init__(\n        self, name: str, level: int = logging.NOTSET, forbidden_keys: set[str] | None = None\n    ) -\u003e None:\n        # Merge parent's keys with keys from child\n        parent_keys = {\"parent_forbidden_key\"}\n        if forbidden_keys:\n            parent_keys = parent_keys | forbidden_keys\n        super().__init__(name, level, forbidden_keys=parent_keys)\n\nclass ChildLogger(ParentLogger):\n    def __init__(self, name: str, level: int = logging.NOTSET) -\u003e None:\n        # Pass child's keys to parent, which will merge them\n        super().__init__(name, level, forbidden_keys={\"child_forbidden_key\"})\n```\n\nIn this example, the final set of forbidden keys will be:\n\n- Builtin `logging.LogRecord` attributes (always present)\n- `parent_forbidden_key` (from `ParentLogger`)\n- `child_forbidden_key` (from `ChildLogger`)\n\nAll keys are merged together - they are not replaced, only supplemented.\n\n### Important notes\n\n- The builtin set of forbidden keys (standard `logging.LogRecord` attributes)\n  is always present and cannot be replaced or removed\n- Additional forbidden keys are merged with the builtin set, not replaced\n- Each subclass must implement the logic to pass `forbidden_keys` to its parent\n  if it wants to propagate keys from its own subclasses\n- The `forbidden_keys` parameter is optional - if not provided, only builtin\n  keys are used, maintaining 100% backward compatibility\n- `None` and empty `set()` are semantically equivalent for `forbidden_keys` -\n  both mean \"no additional forbidden keys\" and produce the same result\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisafronov%2Fpython-logging-objects-with-schema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdisafronov%2Fpython-logging-objects-with-schema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisafronov%2Fpython-logging-objects-with-schema/lists"}