semlog 0.2.0__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.
semlog-0.2.0/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
semlog-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: semlog
3
+ Version: 0.2.0
4
+ Summary: Zero-dependency structured JSON logging for Python, built entirely on the standard library, aligned with OpenTelemetry's log data model and W3C Trace Context.
5
+ Author: flaviopifiator
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: Django
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: System :: Logging
19
+ Requires-Python: >=3.10
20
+ Project-URL: Homepage, https://github.com/flaviopifiator/semlog
21
+ Project-URL: Repository, https://github.com/flaviopifiator/semlog
22
+ Project-URL: Issues, https://github.com/flaviopifiator/semlog/issues
23
+ Project-URL: Changelog, https://github.com/flaviopifiator/semlog/blob/main/CHANGELOG.md
24
+ Description-Content-Type: text/markdown
25
+
26
+ **English** | [Español](README.es.md)
27
+
28
+ <p align="center">
29
+ <picture>
30
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/semlog-logo-dark.svg">
31
+ <img alt="semlog" src="docs/assets/semlog-logo-light.svg" width="360">
32
+ </picture>
33
+ </p>
34
+
35
+ # semlog
36
+
37
+ **Structured JSON logging for Python's standard `logging`: a stable, schema-backed record contract with OpenTelemetry field names, W3C Trace Context and zero dependencies.**
38
+
39
+ [![CI](https://github.com/flaviopifiator/semlog/actions/workflows/ci.yml/badge.svg)](https://github.com/flaviopifiator/semlog/actions/workflows/ci.yml)
40
+ [![Python 3.10-3.14](https://img.shields.io/badge/python-3.10--3.14-3776AB?logo=python&logoColor=white)](.github/workflows/ci.yml)
41
+ [![FastAPI 0.71+](https://img.shields.io/badge/FastAPI-%E2%89%A5%200.71-009688?logo=fastapi&logoColor=white)](.github/workflows/ci.yml)
42
+ [![Django 3.2.9+](https://img.shields.io/badge/Django-%E2%89%A5%203.2.9-092E20?logo=django&logoColor=white)](.github/workflows/ci.yml)
43
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
44
+ [![Runtime dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](pyproject.toml)
45
+ [![Requirements proven: 104/104](https://img.shields.io/badge/requirements%20proven-104%2F104-brightgreen)](STANDARDS.md)
46
+ <!-- Enable after the first PyPI release:
47
+ [![PyPI](https://img.shields.io/pypi/v/semlog)](https://pypi.org/project/semlog/)
48
+ -->
49
+
50
+ ## Why semlog
51
+
52
+ Application code keeps using `logging.getLogger(__name__)`; semlog decides what every record looks like and makes that shape a contract.
53
+
54
+ - **A stable JSON contract.** Field order, types and `null` rules are specified in [STANDARDS.md](STANDARDS.md) and published as a JSON Schema ([`schemas/log-record.schema.json`](schemas/log-record.schema.json)). Removing or renaming a field is a major version change.
55
+ - **OpenTelemetry field names.** `severity_text`, `severity_number`, `trace_id`, `span_id`, `service.*` and `telemetry.sdk.*` follow the OpenTelemetry Logs Data Model, with Semantic Conventions pinned to v1.44.0.
56
+ - **W3C Trace Context built in.** The WSGI and ASGI middlewares read `traceparent`, `tracestate` and allowlisted `baggage`, and `inject()` propagates them on outbound calls. No tracing SDK is needed.
57
+ - **Zero runtime dependencies.** semlog is built entirely on the standard library. Third-party loggers that propagate to the root logger go through the same pipeline, and `capture_loggers` covers the ones that install their own handlers.
58
+ - **Requirement traceability.** Every normative requirement in STANDARDS.md has an ID and at least one test that cites it; the test suite fails when a requirement has no citing test.
59
+
60
+ ## Installation
61
+
62
+ semlog requires Python 3.10 or later. It is not on PyPI yet, so install it from the Git repository:
63
+
64
+ ```bash
65
+ pip install "git+https://github.com/flaviopifiator/semlog.git"
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ Save this as `app.py`:
71
+
72
+ ```python
73
+ import logging
74
+
75
+ import semlog
76
+
77
+ semlog.configure(service_name="checkout")
78
+
79
+ logger = logging.getLogger(__name__)
80
+ logger.info("order.created", extra={"app.order.id": "ord_42", "app.order.total": 1500})
81
+ ```
82
+
83
+ Run `python app.py`. It prints one JSON line:
84
+
85
+ ```json
86
+ {"timestamp":"2026-09-14T18:49:31.659966Z","severity_text":"INFO","severity_number":9,"event_name":"order.created","body":null,"otel.scope.name":"__main__","app.order.id":"ord_42","app.order.total":1500,"service.name":"checkout","service.namespace":null,"service.version":null,"service.instance.id":"b821b60b-7f5e-44a9-88f8-7cf734286abe","deployment.environment.name":null,"telemetry.sdk.name":"semlog","telemetry.sdk.version":"0.2.0","telemetry.sdk.language":"python"}
87
+ ```
88
+
89
+ Three rules keep records useful:
90
+
91
+ - The message is a static event name, such as `order.created`, never an f-string.
92
+ - Variables go in `extra`, under `app.` or an OpenTelemetry key such as `url.path`.
93
+ - `logger.exception(...)` is called once, where the exception is handled.
94
+
95
+ ### Public API
96
+
97
+ semlog exposes exactly eight names. Everything else stays standard `logging`.
98
+
99
+ | Name | Use it to |
100
+ |---|---|
101
+ | `configure(...)` | Configure the process once, at startup |
102
+ | `WSGIMiddleware(app)` | Wrap a WSGI application: trace context and `http.request.id` on every record of a request |
103
+ | `ASGIMiddleware(app)` | The same, for an ASGI application |
104
+ | `operation(headers=None)` | Correlate work outside HTTP, such as jobs, queue consumers and CLI commands |
105
+ | `bind(attributes)` | Add fields to every later record of the current request or operation |
106
+ | `inject(headers, *, trusted=True)` | Propagate trace context to an outbound call |
107
+ | `flush(timeout=None)` | Wait until every queued record is written, before `os._exit()` or in tests |
108
+ | `llm()` | Return the agent guide bundled with the installed version (also `python -m semlog llm`) |
109
+
110
+ Full signatures and semantics are in the [agent guide](src/semlog/agent_guide.md).
111
+
112
+ ## Framework recipes
113
+
114
+ ### FastAPI
115
+
116
+ ```python
117
+ import logging
118
+
119
+ import semlog
120
+ from fastapi import FastAPI
121
+ from semlog import ASGIMiddleware
122
+
123
+ semlog.configure(service_name="my-fastapi-service")
124
+
125
+ app = FastAPI()
126
+ app = ASGIMiddleware(app, log_requests=True)
127
+ # log_requests=True emits one http.server.request event per request (INFO on
128
+ # success, ERROR when the application raises an unhandled exception), carrying
129
+ # http.request.method, url.path, http.response.status_code and event.duration
130
+ # (nanoseconds). It never includes url.query. Default is False: no extra event.
131
+
132
+ logger = logging.getLogger(__name__)
133
+
134
+
135
+ @app.get("/orders/{order_id}")
136
+ async def get_order(order_id: str):
137
+ logger.info("order.lookup.started", extra={"app.order.id": order_id})
138
+ ...
139
+
140
+
141
+ @app.get("/reports/{report_id}")
142
+ def generate_report_sync(report_id: str):
143
+ # A sync `def` endpoint also keeps trace context: Starlette runs it in a
144
+ # threadpool worker with an explicit contextvars.copy_context().
145
+ logger.info("report.generation.started", extra={"app.report.id": report_id})
146
+ ...
147
+ ```
148
+
149
+ ### Django
150
+
151
+ Wrap the WSGI or ASGI entry point, and turn off Django's own logging configuration so it does not replace the root handler that `configure()` installs.
152
+
153
+ ```python
154
+ # wsgi.py
155
+ import semlog
156
+ from django.core.wsgi import get_wsgi_application
157
+ from semlog import WSGIMiddleware
158
+
159
+ semlog.configure(service_name="my-django-service")
160
+ application = WSGIMiddleware(get_wsgi_application(), log_requests=True)
161
+ ```
162
+
163
+ ```python
164
+ # asgi.py
165
+ import semlog
166
+ from django.core.asgi import get_asgi_application
167
+ from semlog import ASGIMiddleware
168
+
169
+ semlog.configure(service_name="my-django-service")
170
+ application = ASGIMiddleware(get_asgi_application(), log_requests=True)
171
+ ```
172
+
173
+ ```python
174
+ # settings.py
175
+ LOGGING_CONFIG = None # Django must not replace the root handler configure() installs
176
+ ```
177
+
178
+ ## Configuration
179
+
180
+ Every `configure()` parameter is keyword-only; there is no settings object or dictionary. An invalid value or combination raises `ValueError` immediately, at startup, never later at runtime.
181
+
182
+ | Group | Parameter | Default | Notes |
183
+ |---|---|---|---|
184
+ | Identity | `service_name` | detected | parameter > `OTEL_SERVICE_NAME` > `OTEL_RESOURCE_ATTRIBUTES` > `pyproject.toml` |
185
+ | Identity | `service_version` | detected | parameter > `OTEL_RESOURCE_ATTRIBUTES` > installed package version > `pyproject.toml` |
186
+ | Identity | `service_namespace` | `None` | parameter > `OTEL_RESOURCE_ATTRIBUTES` |
187
+ | Identity | `service_instance_id` | one UUIDv4 per process | parameter > `OTEL_RESOURCE_ATTRIBUTES` |
188
+ | Identity | `environment` | `None` | parameter > `OTEL_RESOURCE_ATTRIBUTES` |
189
+ | Identity | `identity` | `None` | one value per level named in `identity_levels` |
190
+ | Identity | `identity_levels` | `("role", "component")` | level names composed into one `{namespace}.identity` field |
191
+ | Output | `level` | `"INFO"` | effective root logger level |
192
+ | Output | `namespace` | `"app"` | root of the custom attribute namespace |
193
+ | Output | `capture_loggers` | `()` | third-party loggers whose own handlers are removed, with propagation turned on |
194
+ | Output | `search_dir` | `None` (current working directory) | directory that `pyproject.toml` detection searches upward from |
195
+ | Privacy | `redact_keys` | `()` | added to the built-in redaction list, which cannot be turned off |
196
+ | Distributed context | `baggage_allow` | `()` | baggage keys copied into attributes; process-wide default for the middlewares and `operation()` |
197
+ | Distributed context | `baggage_prefix` | `"baggage."` | attribute-key prefix for copied baggage members |
198
+ | Distributed context | `accept_inbound_baggage` | `True` | `False` ignores an inbound `baggage` header entirely, for public trust boundaries |
199
+ | Limits | `max_attributes` | 128 (or environment variable) | see STANDARDS.md section 6 |
200
+ | Limits | `max_attribute_length` | no limit (or environment variable) | see STANDARDS.md section 6 |
201
+ | Transport | `queue` | `True` | `False` writes synchronously, with no internal queue and no writer thread |
202
+ | Transport | `queue_size` | `10000` | maximum number of queued lines |
203
+ | Transport | `overflow` | `"block"` | `"block"` or `"drop"`, see [Queue overflow](#queue-overflow) |
204
+ | Mode | `mode` | `None` (resolves to `"full"`) | `"full"`, `"hybrid"` or `"off"`; parameter > `SEMLOG_MODE` > `[tool.semlog].mode` (3.11+) > `"full"` |
205
+ | Catalog | `catalog` | `None` | event catalog document (JSON) |
206
+ | Catalog | `catalog_mode` | `"off"` without a catalog, `"warn"` with one | `"off"`, `"warn"` or `"strict"` |
207
+
208
+ ### Precedence and environment variables
209
+
210
+ For the identity and limit parameters below, precedence, from highest to lowest, is always:
211
+
212
+ 1. an explicit `configure()` parameter;
213
+ 2. the `OTEL_*` environment variable;
214
+ 3. `pyproject.toml`, on Python 3.11 and later only;
215
+ 4. the built-in default.
216
+
217
+ `mode` follows its own precedence chain instead, described in [Modes](#modes).
218
+
219
+ Recognized environment variables:
220
+
221
+ | Variable | Sets |
222
+ |---|---|
223
+ | `OTEL_SERVICE_NAME` | `service.name` (`service_name`) |
224
+ | `OTEL_RESOURCE_ATTRIBUTES` | `service.namespace` (`service_namespace`), `service.version` (`service_version`), `service.instance.id` (`service_instance_id`) and `deployment.environment.name` (`environment`); also the fallback for `service.name` (`service_name`) |
225
+ | `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT`, `OTEL_ATTRIBUTE_COUNT_LIMIT` | the `max_attributes` limit |
226
+ | `OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT`, `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` | the `max_attribute_length` limit |
227
+ | `SEMLOG_MODE` | `mode` (see [Modes](#modes)) |
228
+
229
+ When both variables of a limit are set, the `OTEL_LOGRECORD_*` variable wins over the generic `OTEL_ATTRIBUTE_*` one.
230
+
231
+ `pyproject.toml` detection:
232
+
233
+ - On Python 3.11 and later, `service_name` and `service_version` are read from `[project]` with the standard library's `tomllib`; `service_name` falls back to `[tool.poetry]`.
234
+ - Python 3.10 has no `tomllib`, so on 3.10 `pyproject.toml` is not read and a startup diagnostic says why.
235
+
236
+ ### Queue overflow
237
+
238
+ Each record is rendered on the calling thread and queued for a single writer thread that writes it to `stdout`. When the queue is full, `overflow` decides what happens:
239
+
240
+ | Mode | Behavior | Use it when |
241
+ |---|---|---|
242
+ | `overflow="block"` (default) | The call waits for room in the queue; no record is lost | No record may be lost and an occasional wait is acceptable |
243
+ | `overflow="drop"` | The call never waits. At 90% capacity, records below `WARNING` are dropped; the last 10% is reserved for `WARNING`, `ERROR` and `CRITICAL`. Every drop is counted and reported | Application latency matters more than log completeness |
244
+ | `queue=False` | Synchronous write, with no queue and no writer thread | Short scripts, debugging, environments without threads |
245
+
246
+ ## Modes
247
+
248
+ `mode` selects how much of semlog is active in a process: `"full"` (the default, and the right choice for a new service), `"hybrid"`, and `"off"`.
249
+
250
+ For a service already running in production, with its own existing log lines, `hybrid` is the adoption path: every line the service already prints keeps printing byte-identically, and a call written with the `semlog=True` keyword is hidden from that same printed output and instead becomes one JSON record. `logger.info("event.name", extra={...}, semlog=True)` is the shape of a call that adopts semlog this way. `off` behaves as if semlog were never installed at all, except that the keyword itself never raises, so it stays safe to leave in call sites while rolling back. `full` is the end state, and the default for a new service that has no existing log lines to preserve: the adoption path is `hybrid`, then `full` once its output has been reviewed. `mode` is switched by environment, with no code change required.
251
+
252
+ - **`full`**: every log call goes through semlog's pipeline and becomes one JSON record per line, exactly as shown in [Output](#output).
253
+ - **`hybrid`**: `configure()` never touches the root logger's existing handlers or level. A call made with `semlog=True` is hidden from every `StreamHandler` and instead emitted as one semlog JSON record; every other call keeps printing exactly as it did before semlog was installed. See [Limitations](#limitations) for the exact scope of this suppression.
254
+ - **`off`**: `configure()` installs nothing and does not touch the root logger. `semlog=True` still never raises, but produces no JSON output and no other side effect of its own; `operation()`, `bind()` and both middlewares keep working as inert pass-throughs.
255
+
256
+ ### Configuration sources
257
+
258
+ `mode` resolves from, in order of precedence:
259
+
260
+ 1. the `mode` keyword argument to `configure()`;
261
+ 2. the `SEMLOG_MODE` environment variable;
262
+ 3. the `mode` key under `[tool.semlog]` in `pyproject.toml`;
263
+ 4. the default, `"full"`.
264
+
265
+ ```toml
266
+ [tool.semlog]
267
+ mode = "hybrid"
268
+ ```
269
+
270
+ Reading `pyproject.toml` needs the standard library's `tomllib`, available on Python 3.11 and later; on Python 3.10 this source is skipped entirely, and resolution falls through to the next one. The file is searched starting from the current working directory (or `configure(search_dir=...)`, when given) and upward through its parent directories; a container image built without the project's source tree present, or without the working directory set to it, often has no `pyproject.toml` to find, in which case this source is silently skipped, the same as on Python 3.10. An empty `SEMLOG_MODE` is treated as absent, the same as leaving it unset, and falls through to the next source; this does not apply to `[tool.semlog].mode`, where an empty string is a real declared value. A value that is present but outside `"full"`, `"hybrid"`, `"off"`, from any of the three sources, raises `ValueError` naming both the invalid value and the source it came from.
271
+
272
+ ### Limitations
273
+
274
+ - **Hybrid's suppression is scoped to `StreamHandler.handle`.** Only `logging.StreamHandler` instances and subclasses that reach that method are hidden from a marked record. A handler outside that synchronous dispatch, such as a `logging.handlers.QueueHandler` paired with a `QueueListener`, or a `logging.handlers.MemoryHandler`, may still render a marked record as text; so may a `StreamHandler` subclass that overrides `handle()` without calling `super().handle()`. None of this is a defect, only the documented edge of what a method-level patch can reach.
275
+ - **`off` is a process-start switch, not a live toggle.** A process that starts in `off` mode (or with `SEMLOG_MODE=off`) behaves as if semlog were never installed. Reconfiguring an already-running process from `full` or `hybrid` to `off` leaves the JSON pipeline already installed on the root logger attached; it does not tear it down.
276
+ - **Uninstalling semlog while marked calls remain raises `TypeError`.** If `semlog` is removed from a service that still has `semlog=True` call sites, an enabled call at that call site fails with `TypeError`, rather than failing silently. Remove the keyword from call sites before uninstalling.
277
+
278
+ ## Output
279
+
280
+ Every record is one JSON object per line, in UTF-8. This record was logged inside an `operation()` that received a `traceparent` header, and is indented here for reading; semlog never indents its output:
281
+
282
+ ```json
283
+ {
284
+ "timestamp": "2026-09-14T18:49:31.719135Z",
285
+ "severity_text": "INFO",
286
+ "severity_number": 9,
287
+ "event_name": "payment.authorization.completed",
288
+ "body": null,
289
+ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
290
+ "span_id": "6914640b649ae6de",
291
+ "trace_flags": "01",
292
+ "http.request.id": "01a0a140-c547-72e5-9455-a8b81f4a94d7",
293
+ "otel.scope.name": "payments.service",
294
+ "app.payment.amount": 15000,
295
+ "app.identity": "api.payments",
296
+ "service.name": "payments",
297
+ "service.namespace": null,
298
+ "service.version": "1.4.0",
299
+ "service.instance.id": "936c21f2-7be1-4006-933a-e84d39621fe7",
300
+ "deployment.environment.name": "production",
301
+ "telemetry.sdk.name": "semlog",
302
+ "telemetry.sdk.version": "0.2.0",
303
+ "telemetry.sdk.language": "python"
304
+ }
305
+ ```
306
+
307
+ Keys are flat, dotted strings. Trace fields appear only when a trace context is bound, and an optional value that is unknown is `null`, never an empty string. Sensitive keys such as `password` or `token` are always replaced by `"REDACTED"`. The full field order, the severity mapping and the presence rules are in [STANDARDS.md](STANDARDS.md), and the formal schema is [`schemas/log-record.schema.json`](schemas/log-record.schema.json).
308
+
309
+ ## Compatibility
310
+
311
+ The test suite runs in CI on CPython 3.10, 3.11, 3.12, 3.13 and 3.14; 3.15 also runs there and is allowed to fail. The package is pure Python (`py3-none-any` wheel). Framework integration is tested in CI against these versions:
312
+
313
+ | Framework | Version | Python | Role |
314
+ |---|---|---|---|
315
+ | FastAPI (Starlette 0.17.1) | 0.71.0 | 3.10 | minimum |
316
+ | FastAPI (Starlette 1.6.0) | 0.141.1 | 3.10, 3.14 | latest |
317
+ | Django | 3.2.9 | 3.10 | minimum |
318
+ | Django | 5.2 LTS | 3.10, 3.14 | latest |
319
+ | Django | 6.1 | 3.12, 3.14 | latest |
320
+
321
+ FastAPI is tested with `async def` and sync `def` endpoints. Django is tested with sync views over WSGI and with async and sync views over ASGI.
322
+
323
+ ## Documentation
324
+
325
+ - [STANDARDS.md](STANDARDS.md): the normative record contract, severity mapping, extension limits, pipeline guarantees and compatibility policy, with requirement IDs and a traceability annex.
326
+ - [`schemas/log-record.schema.json`](schemas/log-record.schema.json) and [`schemas/event-catalog.schema.json`](schemas/event-catalog.schema.json): JSON Schema 2020-12 for every record and for the event catalog.
327
+ - [Agent guide](src/semlog/agent_guide.md): the call-site reference for coding agents, with rules, migration and review checklists, shipped inside the package (`python -m semlog llm`). [llms.txt](llms.txt) is the agent-facing entry point to the repository.
328
+ - [SECURITY.md](SECURITY.md): how to report a vulnerability privately.
329
+ - [CHANGELOG.md](CHANGELOG.md): notable changes, in the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. semlog follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
330
+
331
+ ## Contributing
332
+
333
+ Contributions are welcome as pull requests to `main`. Commit messages follow Conventional Commits, and every code change follows strict test-driven development. [AGENTS.md](AGENTS.md) has the setup, test, lint and build commands, the tooling policy and the branch protection rules. Report security vulnerabilities privately, as described in [SECURITY.md](SECURITY.md), never in a public issue.
334
+
335
+ ## License
336
+
337
+ semlog is licensed under the [Apache License 2.0](LICENSE) (SPDX: `Apache-2.0`).