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.
- logging_mixin-0.1.0/.gitignore +131 -0
- logging_mixin-0.1.0/LICENSE +149 -0
- logging_mixin-0.1.0/PKG-INFO +352 -0
- logging_mixin-0.1.0/README.md +317 -0
- logging_mixin-0.1.0/pyproject.toml +61 -0
- logging_mixin-0.1.0/src/logging_mixin/__init__.py +60 -0
- logging_mixin-0.1.0/src/logging_mixin/adapters/__init__.py +10 -0
- logging_mixin-0.1.0/src/logging_mixin/adapters/aws_lambda.py +89 -0
- logging_mixin-0.1.0/src/logging_mixin/adapters/django.py +95 -0
- logging_mixin-0.1.0/src/logging_mixin/adapters/fastapi.py +137 -0
- logging_mixin-0.1.0/src/logging_mixin/context.py +84 -0
- logging_mixin-0.1.0/src/logging_mixin/mixin.py +136 -0
- logging_mixin-0.1.0/tests/__init__.py +0 -0
- logging_mixin-0.1.0/tests/test_classmethod_constraint.py +121 -0
- logging_mixin-0.1.0/tests/test_context.py +66 -0
- logging_mixin-0.1.0/tests/test_mixin.py +214 -0
|
@@ -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
|