logging-mixin 0.1.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.
@@ -0,0 +1,131 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # Jupyter Notebook
76
+ .ipynb_checkpoints
77
+
78
+ # IPython
79
+ profile_default/
80
+ ipython_config.py
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # pipenv
86
+ Pipfile.lock
87
+
88
+ # PEP 582
89
+ __pypackages__/
90
+
91
+ # Celery stuff
92
+ celerybeat-schedule
93
+ celerybeat.pid
94
+
95
+ # SageMath parsed files
96
+ *.sage.py
97
+
98
+ # Environments
99
+ .env
100
+ .venv
101
+ env/
102
+ venv/
103
+ ENV/
104
+ env.bak/
105
+ venv.bak/
106
+
107
+ # Spyder project settings
108
+ .spyderproject
109
+ .spyproject
110
+
111
+ # Rope project settings
112
+ .ropeproject
113
+
114
+ # mkdocs documentation
115
+ /site
116
+
117
+ # mypy
118
+ .mypy_cache/
119
+ .dmypy.json
120
+ dmypy.json
121
+
122
+ # Pyre type checker
123
+ .pyre/
124
+
125
+ # IDE
126
+ .vscode/
127
+ .idea/
128
+ *.swp
129
+ *.swo
130
+ *~
131
+ .DS_Store
@@ -0,0 +1,149 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+
4
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
5
+
6
+ 1. Definitions.
7
+
8
+ "License" shall mean the terms and conditions for use, reproduction,
9
+ and distribution as defined in Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the
12
+ copyright owner that is granting the License.
13
+
14
+ "Legal Entity" shall mean the union of the acting entity and all other
15
+ entities that control, are controlled by, or are under common control
16
+ with that entity. For the purposes of this definition, "control" means
17
+ (i) the power, direct or indirect, to cause the direction or management
18
+ of such entity, whether by contract or otherwise, or (ii) ownership of
19
+ fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
20
+ ownership of such entity.
21
+
22
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
23
+ permissions granted by this License.
24
+
25
+ "Source" form shall mean the preferred form for making modifications,
26
+ including but not limited to software source code, documentation source,
27
+ and configuration files.
28
+
29
+ "Object" form shall mean any form resulting from mechanical transformation
30
+ or translation of a Source form, including but not limited to compiled
31
+ object code, generated documentation, and conversions to other media types.
32
+
33
+ "Work" shall mean the work of authorship, whether in Source or Object
34
+ form, made available under the License, as indicated by a copyright notice
35
+ that is included in or attached to the work (an example is provided in
36
+ the Appendix below).
37
+
38
+ "Derivative Works" shall mean any work, whether in Source or Object form,
39
+ that is based on (or derived from) the Work and for which the editorial
40
+ revisions, annotations, elaborations, or other modifications represent
41
+ originally authored expressions of authorship. For the purposes of this
42
+ License, Derivative Works shall not include works that remain separable
43
+ from, or merely link (or bind by name) to the interfaces of, the Work and
44
+ Derivative Works thereof.
45
+
46
+ "Contribution" shall mean any work of authorship, including the original
47
+ version of the Work and any derivative works thereof, and any modifications
48
+ to those works or derivative works, including without limitation any
49
+ additions to that Work or Derivative Work that constitutes, in whole or
50
+ in part, an original work of authorship.
51
+
52
+ "Contributor" shall mean Licensor and any individual or Legal Entity on
53
+ behalf of whom a Contribution has been received by Licensor and subsequently
54
+ incorporated within the Work.
55
+
56
+ 2. Grant of Copyright License. Subject to the terms and conditions of this
57
+ License, each Contributor hereby grants to You a perpetual, worldwide,
58
+ non-exclusive, no-charge, royalty-free, irrevocable copyright license to
59
+ reproduce, prepare Derivative Works of, publicly display, publicly perform,
60
+ sublicense, and distribute the Work and such Derivative Works in Source or
61
+ Object form.
62
+
63
+ 3. Grant of Patent License. Subject to the terms and conditions of this License,
64
+ each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
65
+ no-charge, royalty-free, irrevocable (except as stated in this section)
66
+ patent license to make, have made, use, offer to sell, sell, import, and
67
+ otherwise transfer the Work, where such license applies only to those patent
68
+ claims licensable by such Contributor that are necessarily infringed by their
69
+ Contribution(s) alone or by combination of their Contribution(s) with the
70
+ Work to which such Contribution was submitted. If You institute patent
71
+ litigation against any entity (including a cross-claim or counterclaim in
72
+ a lawsuit) alleging that the Work or a Contribution incorporated within the
73
+ Work constitutes direct or contributory patent infringement, then any patent
74
+ licenses granted to You under this License for that Work shall terminate as
75
+ of the date such litigation is filed.
76
+
77
+ 4. Redistribution. You may reproduce and distribute copies of the Work or
78
+ Derivative Works thereof in any medium, with or without modifications, and
79
+ in Source or Object form, provided that You meet the following conditions:
80
+
81
+ (a) You must give any other recipients of the Work or Derivative Works a
82
+ copy of this License; and
83
+
84
+ (b) You must cause any modified files to carry prominent notices stating
85
+ that You changed the files; and
86
+
87
+ (c) You must retain, in the Source form of any Derivative Works that You
88
+ distribute, all copyright, patent, trademark, and attribution notices
89
+ from the Source form of the Work, excluding those notices that do not
90
+ pertain to any part of the Derivative Works; and
91
+
92
+ (d) If the Work includes a "NOTICE" text file, then any Derivative Works
93
+ that You distribute must include a readable copy of the attribution
94
+ notices contained within such NOTICE file, excluding those notices that
95
+ do not pertain to any part of the Derivative Works.
96
+
97
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any
98
+ Contribution intentionally submitted for inclusion in the Work by You to
99
+ Licensor shall be under the terms and conditions of this License, without
100
+ any additional terms or conditions. Notwithstanding the above, nothing herein
101
+ shall supersede or modify the terms of any separate license agreement you
102
+ may have executed with Licensor regarding such Contribution.
103
+
104
+ 6. Trademarks. This License does not grant permission to use the trade names,
105
+ trademarks, service marks, or product names of the Licensor, except as
106
+ required for reasonable and customary use in describing the origin of the
107
+ Work and reproducing the content of the NOTICE file.
108
+
109
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
110
+ writing, Licensor provides the Work (and each Contributor provides its
111
+ Contributions) on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
112
+ KIND, either express or implied, including without limitation any warranties
113
+ or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
114
+ PARTICULAR PURPOSE. You are solely responsible for determining the
115
+ appropriateness of using or redistributing the Work and assume any risks
116
+ associated with Your exercise of permissions under this License.
117
+
118
+ 8. Limitation of Liability. In no event and under no legal theory, whether in
119
+ tort (including negligence), contract, or otherwise, unless required by
120
+ applicable law (such as deliberate and grossly negligent acts) or agreed to
121
+ in writing, shall any Contributor be liable to You for damages, including
122
+ any direct, indirect, special, incidental, or consequential damages of any
123
+ character arising as a result of this License or out of the use or inability
124
+ to use the Work.
125
+
126
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or
127
+ Derivative Works thereof, You may choose to offer, and charge a fee for,
128
+ acceptance of support, warranty, indemnity, or other liability obligations
129
+ and/or rights consistent with this License. However, in accepting such
130
+ obligations, You may act only on Your own behalf and on Your sole
131
+ responsibility, not on behalf of any other Contributor, and only if You
132
+ agree to indemnify, defend, and hold each Contributor harmless for any
133
+ liability incurred by, or claims asserted against, such Contributor by
134
+ reason of your accepting any such warranty or additional liability.
135
+
136
+ END OF TERMS AND CONDITIONS
137
+
138
+ Copyright 2026 James Ekhator
139
+
140
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not
141
+ use this file except in compliance with the License. You may obtain a copy of
142
+ the License at
143
+
144
+ http://www.apache.org/licenses/LICENSE-2.0
145
+
146
+ Unless required by applicable law or agreed to in writing, software distributed
147
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
148
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
149
+ specific language governing permissions and limitations under the License.
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: logging-mixin
3
+ Version: 0.1.0
4
+ Summary: Class-bound structured logging with auto-injected correlation IDs for Python services.
5
+ Project-URL: Homepage, https://github.com/jekhator/logging-mixin
6
+ Project-URL: Repository, https://github.com/jekhator/logging-mixin.git
7
+ Project-URL: Issues, https://github.com/jekhator/logging-mixin/issues
8
+ Author: James Ekhator
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: correlation-id,distributed-tracing,logging,observability,structured-logging
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: aws-lambda
25
+ Provides-Extra: dev
26
+ Requires-Dist: django>=4.2; extra == 'dev'
27
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
28
+ Requires-Dist: pytest-cov; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Provides-Extra: django
31
+ Requires-Dist: django>=4.2; extra == 'django'
32
+ Provides-Extra: fastapi
33
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # logging-mixin
37
+
38
+ **Class-bound structured logging with auto-injected correlation IDs for Python services.**
39
+
40
+ Replaces module-level loggers in business logic with per-class loggers that automatically inject correlation IDs and class context into every log record. Enables clean distributed tracing without boilerplate.
41
+
42
+ ## Why?
43
+
44
+ Production services need correlation IDs for tracing requests through distributed systems. Traditional approaches require passing context through every function or managing global state:
45
+
46
+ ```python
47
+ # Traditional approach: manual context passing (boilerplate)
48
+ logger = logging.getLogger(__name__)
49
+
50
+ def create_user(name: str, correlation_id: str):
51
+ logger.info("Creating user", extra={"correlation_id": correlation_id, "name": name})
52
+ # Must thread correlation_id through every call
53
+ save_user(name, correlation_id)
54
+
55
+ def save_user(name: str, correlation_id: str):
56
+ logger.info("Saving user", extra={"correlation_id": correlation_id, "name": name})
57
+ ```
58
+
59
+ `logging-mixin` solves this with automatic injection:
60
+
61
+ ```python
62
+ from logging_mixin import LoggingMixin
63
+
64
+ class UserService(LoggingMixin):
65
+ def create_user(self, name: str):
66
+ self.log_info("Creating user", name=name)
67
+ # correlation_id automatically injected by LoggingMixin
68
+ self.save_user(name)
69
+
70
+ def save_user(self, name: str):
71
+ self.log_info("Saving user", name=name)
72
+ # correlation_id still available, no parameter needed
73
+ ```
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ pip install logging-mixin
79
+ ```
80
+
81
+ Requires Python 3.10+.
82
+
83
+ ### Optional framework extras
84
+
85
+ ```bash
86
+ # Django support
87
+ pip install logging-mixin[django]
88
+
89
+ # FastAPI support
90
+ pip install logging-mixin[fastapi]
91
+
92
+ # AWS Lambda support
93
+ pip install logging-mixin[aws-lambda]
94
+
95
+ # All together
96
+ pip install logging-mixin[django,fastapi,aws-lambda]
97
+
98
+ # Development (includes all extras + testing tools)
99
+ pip install logging-mixin[dev]
100
+ ```
101
+
102
+ ## Quick Start
103
+
104
+ ### 1. Create a service using LoggingMixin
105
+
106
+ ```python
107
+ from logging_mixin import LoggingMixin
108
+
109
+ class OrderService(LoggingMixin):
110
+ def create_order(self, user_id: int, items: list[str]):
111
+ self.log_info("order.create", user_id=user_id, item_count=len(items))
112
+ # Logs with:
113
+ # logger name: "myapp.services.OrderService"
114
+ # extra: {"correlation_id": "abc123", "user_id": 123, "item_count": 3}
115
+ ...
116
+ ```
117
+
118
+ ### 2. Set correlation ID in your framework
119
+
120
+ #### Django
121
+
122
+ Add the middleware to `settings.py`:
123
+
124
+ ```python
125
+ MIDDLEWARE = [
126
+ "logging_mixin.adapters.django.CorrelationIdMiddleware",
127
+ # ... other middleware
128
+ ]
129
+ ```
130
+
131
+ #### FastAPI
132
+
133
+ Add middleware:
134
+
135
+ ```python
136
+ from fastapi import FastAPI
137
+ from logging_mixin.adapters.fastapi import correlation_id_middleware
138
+
139
+ app = FastAPI()
140
+ app.add_middleware(correlation_id_middleware)
141
+ ```
142
+
143
+ #### AWS Lambda
144
+
145
+ Call in handler:
146
+
147
+ ```python
148
+ from logging_mixin.adapters.aws_lambda import setup_correlation_id
149
+
150
+ def lambda_handler(event, context):
151
+ setup_correlation_id(event, context)
152
+ # Now LoggingMixin can access correlation_id
153
+ ...
154
+ ```
155
+
156
+ ### 3. Use in background tasks
157
+
158
+ Correlation IDs automatically propagate to Celery tasks, background jobs, and async functions via Python's `contextvars`:
159
+
160
+ ```python
161
+ from celery import shared_task
162
+ from logging_mixin import LoggingMixin
163
+
164
+ @shared_task
165
+ def process_order(order_id: int):
166
+ service = OrderService()
167
+ service.log_info("order.processing", order_id=order_id)
168
+ # Inherits correlation_id from the original request context
169
+ ```
170
+
171
+ ## Design
172
+
173
+ ### Instance-only (no @classmethod/@staticmethod)
174
+
175
+ LoggingMixin's `log_*` methods are instance methods. They cannot be called from `@classmethod` or `@staticmethod` because they read `self._logger`:
176
+
177
+ ```python
178
+ class MyService(LoggingMixin):
179
+ def instance_method(self):
180
+ self.log_info("works") # ✓ OK
181
+
182
+ @classmethod
183
+ def class_method(cls):
184
+ self.log_info("FAILS") # ✗ TypeError: missing 1 required positional argument 'self'
185
+
186
+ @staticmethod
187
+ def static_method():
188
+ self.log_info("FAILS") # ✗ TypeError
189
+ ```
190
+
191
+ **Workaround:** Use module-level logger for class methods and manually inject correlation ID:
192
+
193
+ ```python
194
+ import logging
195
+ from logging_mixin import get_correlation_id
196
+
197
+ logger = logging.getLogger(__name__)
198
+
199
+ class MyService(LoggingMixin):
200
+ def instance_method(self):
201
+ self.log_info("instance.event") # ✓ Automatic injection
202
+
203
+ @classmethod
204
+ def class_method(cls):
205
+ cid = get_correlation_id()
206
+ logger.info("class.event", extra={"correlation_id": cid or "-"}) # ✓ Manual injection
207
+ ```
208
+
209
+ ### Correlation ID lifecycle
210
+
211
+ Correlation IDs are stored in a `contextvars.ContextVar`, which means they:
212
+ - Survive async/await boundaries (async-safe)
213
+ - Cross thread boundaries when using thread pool executors
214
+ - Are automatically reset at the start of each HTTP request (Django/FastAPI middleware)
215
+ - Propagate to background tasks (Celery, threading, async)
216
+
217
+ ### Composition with masking
218
+
219
+ LoggingMixin automatically composes with masking libraries. If your instance has a `mask_for_logging()` method (e.g., from a masking mixin), its output is added to the log record:
220
+
221
+ ```python
222
+ from logging_mixin import LoggingMixin
223
+ from some_library import MaskingMixin
224
+
225
+ class Response(LoggingMixin, MaskingMixin):
226
+ def trace(self):
227
+ self.log_debug("response")
228
+ # Logs with extra: {"correlation_id": "...", "instance": <masked dict>}
229
+ ```
230
+
231
+ ## API Reference
232
+
233
+ ### LoggingMixin
234
+
235
+ Class-bound logger providing five methods:
236
+
237
+ ```python
238
+ class Service(LoggingMixin):
239
+ def do_thing(self):
240
+ self.log_debug("event", detail="verbose") # DEBUG level
241
+ self.log_info("event", status="ok") # INFO level
242
+ self.log_warning("event", issue="slow") # WARNING level
243
+ self.log_error("event", error="failure") # ERROR level
244
+ self.log_exception("event") # ERROR level + traceback
245
+ ```
246
+
247
+ Each method:
248
+ - Takes an event name (string) and optional keyword arguments
249
+ - Automatically injects correlation_id into the log record's `extra` dict
250
+ - Reads from the per-class logger (`module.ClassName`)
251
+ - Composes with `mask_for_logging()` if available
252
+
253
+ ### Context functions
254
+
255
+ ```python
256
+ from logging_mixin import get_correlation_id, set_correlation_id, clear_correlation_id
257
+
258
+ # Get the current correlation ID (returns None if not set)
259
+ cid = get_correlation_id()
260
+
261
+ # Manually set (for background tasks, tests, non-request contexts)
262
+ set_correlation_id("abc123def456")
263
+
264
+ # Clear (useful for test isolation)
265
+ clear_correlation_id()
266
+ ```
267
+
268
+ ### Framework adapters
269
+
270
+ #### Django middleware
271
+
272
+ Automatically sets correlation ID from `X-Correlation-ID` header or generates UUID.
273
+
274
+ ```python
275
+ from logging_mixin.adapters.django import CorrelationIdMiddleware
276
+
277
+ MIDDLEWARE = ["logging_mixin.adapters.django.CorrelationIdMiddleware", ...]
278
+ ```
279
+
280
+ #### FastAPI dependency
281
+
282
+ Two approaches:
283
+
284
+ ```python
285
+ # Middleware (auto for all routes):
286
+ from logging_mixin.adapters.fastapi import correlation_id_middleware
287
+ app.add_middleware(correlation_id_middleware)
288
+
289
+ # Or dependency (per-route opt-in):
290
+ from fastapi import Depends
291
+ from logging_mixin.adapters.fastapi import correlation_id_dependency
292
+
293
+ @app.get("/items/")
294
+ def get_items(cid: str = Depends(correlation_id_dependency)):
295
+ ...
296
+ ```
297
+
298
+ #### AWS Lambda
299
+
300
+ ```python
301
+ from logging_mixin.adapters.aws_lambda import setup_correlation_id
302
+
303
+ def lambda_handler(event, context):
304
+ setup_correlation_id(event, context)
305
+ # Now LoggingMixin can access correlation_id via get_correlation_id()
306
+ ...
307
+ ```
308
+
309
+ ## Testing
310
+
311
+ LoggingMixin is test-friendly:
312
+
313
+ ```python
314
+ import logging
315
+ from logging_mixin import LoggingMixin, set_correlation_id
316
+
317
+ class TestMyService:
318
+ def test_logs_with_correlation_id(self, caplog):
319
+ set_correlation_id("test-123")
320
+
321
+ service = MyService()
322
+ with caplog.at_level(logging.INFO):
323
+ service.do_something()
324
+
325
+ assert caplog.records[0].correlation_id == "test-123"
326
+ ```
327
+
328
+ ## Design Principles
329
+
330
+ - **Class-bound:** Each class gets its own logger (`module.ClassName`) for clean grouping
331
+ - **Instance-only:** Methods read `self._logger` (cannot be used from @classmethod/@staticmethod)
332
+ - **Async-safe:** Uses `contextvars.ContextVar` (survives async/await, thread pools)
333
+ - **Framework-agnostic:** Core mixin has zero framework dependencies
334
+ - **Composable:** Works naturally with masking mixins and other mixins
335
+ - **Zero boilerplate:** No function signature changes needed
336
+
337
+ ## Trade-offs
338
+
339
+ - **Cannot use in @classmethod/@staticmethod:** Use module-level logger + manual correlation ID injection instead
340
+ - **Requires ContextVar setup:** Framework adapters or manual `set_correlation_id()` call needed
341
+ - **Implicit behavior:** Correlation ID is silently injected (can be surprising if not expected)
342
+
343
+ ## License
344
+
345
+ Apache 2.0 — see LICENSE file.
346
+
347
+ ## See Also
348
+
349
+ - [contextvars](https://docs.python.org/3/library/contextvars.html) — Python standard library
350
+ - [logging](https://docs.python.org/3/library/logging.html) — Python standard library
351
+ - [Django middleware](https://docs.djangoproject.com/en/stable/topics/http/middleware/) — Django framework
352
+ - [FastAPI middleware](https://fastapi.tiangolo.com/tutorial/middleware/) — FastAPI framework