logctx 0.0.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.
- logctx-0.0.0/.github/workflows/cicd.yml +12 -0
- logctx-0.0.0/.github/workflows/python-checks.yml +75 -0
- logctx-0.0.0/.github/workflows/release.yml +152 -0
- logctx-0.0.0/.gitignore +26 -0
- logctx-0.0.0/CHANGELOG.md +4 -0
- logctx-0.0.0/LICENSE +21 -0
- logctx-0.0.0/PKG-INFO +68 -0
- logctx-0.0.0/README.md +39 -0
- logctx-0.0.0/examples/.gitkeep +0 -0
- logctx-0.0.0/logctx/__init__.py +32 -0
- logctx-0.0.0/logctx/_core.py +165 -0
- logctx-0.0.0/logctx/decorators.py +187 -0
- logctx-0.0.0/logctx/py.typed +0 -0
- logctx-0.0.0/logctx.egg-info/PKG-INFO +68 -0
- logctx-0.0.0/logctx.egg-info/SOURCES.txt +24 -0
- logctx-0.0.0/logctx.egg-info/dependency_links.txt +1 -0
- logctx-0.0.0/logctx.egg-info/requires.txt +1 -0
- logctx-0.0.0/logctx.egg-info/top_level.txt +1 -0
- logctx-0.0.0/pyproject.toml +75 -0
- logctx-0.0.0/setup.cfg +4 -0
- logctx-0.0.0/tests/__init__.py +0 -0
- logctx-0.0.0/tests/test_contextmanager.py +99 -0
- logctx-0.0.0/tests/test_inject_context.py +189 -0
- logctx-0.0.0/tests/test_log_arguments.py +71 -0
- logctx-0.0.0/tests/test_logging_filter.py +51 -0
- logctx-0.0.0/uv.lock +354 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Python Quality Checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
python-versions:
|
|
7
|
+
required: true
|
|
8
|
+
type: string
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
UV_FROZEN: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Ruff Lint (${{ matrix.python-version }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ${{ fromJSON(inputs.python-versions) }}
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install lint dependencies
|
|
30
|
+
run: uv sync --group linting
|
|
31
|
+
|
|
32
|
+
- name: Run ruff
|
|
33
|
+
run: uv run ruff check .
|
|
34
|
+
|
|
35
|
+
typecheck:
|
|
36
|
+
name: Mypy Type Check (${{ matrix.python-version }})
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
strategy:
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix:
|
|
41
|
+
python-version: ${{ fromJSON(inputs.python-versions) }}
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- uses: astral-sh/setup-uv@v6
|
|
47
|
+
with:
|
|
48
|
+
python-version: ${{ matrix.python-version }}
|
|
49
|
+
|
|
50
|
+
- name: Install type-checking dependencies
|
|
51
|
+
run: uv sync --group typechecking
|
|
52
|
+
|
|
53
|
+
- name: Run mypy
|
|
54
|
+
run: uv run mypy .
|
|
55
|
+
|
|
56
|
+
test:
|
|
57
|
+
name: Pytest (${{ matrix.python-version }})
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
strategy:
|
|
60
|
+
fail-fast: false
|
|
61
|
+
matrix:
|
|
62
|
+
python-version: ${{ fromJSON(inputs.python-versions) }}
|
|
63
|
+
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
|
|
67
|
+
- uses: astral-sh/setup-uv@v6
|
|
68
|
+
with:
|
|
69
|
+
python-version: ${{ matrix.python-version }}
|
|
70
|
+
|
|
71
|
+
- name: Install test dependencies
|
|
72
|
+
run: uv sync --group testing
|
|
73
|
+
|
|
74
|
+
- name: Run tests
|
|
75
|
+
run: uv run pytest .
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: Semantic Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
python-checks:
|
|
10
|
+
name: Python Quality Checks
|
|
11
|
+
uses: ./.github/workflows/python-checks.yml
|
|
12
|
+
with:
|
|
13
|
+
python-versions: '["3.9", "3.10", "3.11", "3.12", "3.13"]'
|
|
14
|
+
secrets: inherit
|
|
15
|
+
|
|
16
|
+
release:
|
|
17
|
+
name: Semantic Version Release
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
needs: python-checks
|
|
20
|
+
environment: release
|
|
21
|
+
concurrency:
|
|
22
|
+
group: ${{ github.workflow }}-release-${{ github.ref_name }}
|
|
23
|
+
cancel-in-progress: false
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write
|
|
27
|
+
contents: write
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- name: Setup | Checkout Repository on Release Branch
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
# the checkout action persists the passed credentials by default
|
|
33
|
+
# subsequent git commands will pick them up automatically
|
|
34
|
+
with:
|
|
35
|
+
ref: ${{ github.ref_name }}
|
|
36
|
+
fetch-depth: 0
|
|
37
|
+
token: ${{ secrets.RELEASE_PAT }}
|
|
38
|
+
|
|
39
|
+
- name: Setup | Force release branch to be at workflow sha
|
|
40
|
+
run: |
|
|
41
|
+
git reset --hard ${{ github.sha }}
|
|
42
|
+
|
|
43
|
+
- name: Evaluate | Verify upstream has NOT changed
|
|
44
|
+
shell: bash
|
|
45
|
+
run: |
|
|
46
|
+
set +o pipefail
|
|
47
|
+
|
|
48
|
+
UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)"
|
|
49
|
+
printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME"
|
|
50
|
+
|
|
51
|
+
set -o pipefail
|
|
52
|
+
|
|
53
|
+
if [ -z "$UPSTREAM_BRANCH_NAME" ]; then
|
|
54
|
+
printf >&2 '%s\n' "::error::Unable to determine upstream branch name!"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
git fetch "${UPSTREAM_BRANCH_NAME%%/*}"
|
|
59
|
+
|
|
60
|
+
if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then
|
|
61
|
+
printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
HEAD_SHA="$(git rev-parse HEAD)"
|
|
66
|
+
|
|
67
|
+
if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then
|
|
68
|
+
printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]"
|
|
69
|
+
printf >&2 '%s\n' "::error::Upstream has changed, aborting release..."
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
printf '%s\n' "Verified upstream branch has not changed, continuing with release..."
|
|
74
|
+
|
|
75
|
+
- name: Action | Semantic Version Release
|
|
76
|
+
id: release
|
|
77
|
+
uses: python-semantic-release/python-semantic-release@v9.21.1
|
|
78
|
+
with:
|
|
79
|
+
github_token: ${{ secrets.RELEASE_PAT }}
|
|
80
|
+
git_committer_name: "github-actions"
|
|
81
|
+
git_committer_email: "actions@users.noreply.github.com"
|
|
82
|
+
|
|
83
|
+
- name: Publish | Upload to GitHub Release Assets
|
|
84
|
+
uses: python-semantic-release/publish-action@v9.21.1
|
|
85
|
+
with:
|
|
86
|
+
github_token: ${{ secrets.RELEASE_PAT }}
|
|
87
|
+
tag: ${{ needs.release.outputs.tag }}
|
|
88
|
+
|
|
89
|
+
build:
|
|
90
|
+
name: Build Distribution Packages
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
needs: release
|
|
93
|
+
environment: release
|
|
94
|
+
|
|
95
|
+
steps:
|
|
96
|
+
- name: Setup | Checkout Repository
|
|
97
|
+
uses: actions/checkout@v4
|
|
98
|
+
|
|
99
|
+
- name: Setup | Install Python
|
|
100
|
+
uses: actions/setup-python@v4
|
|
101
|
+
with:
|
|
102
|
+
python-version: '3.x'
|
|
103
|
+
|
|
104
|
+
- name: Build | Create Distribution Packages
|
|
105
|
+
run: |
|
|
106
|
+
python -m pip install --upgrade pip
|
|
107
|
+
pip install build
|
|
108
|
+
python -m build
|
|
109
|
+
|
|
110
|
+
- name: Upload | Upload Distribution Packages
|
|
111
|
+
uses: actions/upload-artifact@v4
|
|
112
|
+
with:
|
|
113
|
+
name: logctx-distributions
|
|
114
|
+
path: dist/
|
|
115
|
+
|
|
116
|
+
testpypi:
|
|
117
|
+
name: Publish to TestPyPI
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
needs: build
|
|
120
|
+
environment: testpypi
|
|
121
|
+
permissions:
|
|
122
|
+
id-token: write
|
|
123
|
+
|
|
124
|
+
steps:
|
|
125
|
+
- name: Download | Download Distribution Packages
|
|
126
|
+
uses: actions/download-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: logctx-distributions
|
|
129
|
+
path: dist/
|
|
130
|
+
|
|
131
|
+
- name: Publish | Upload package to PyPI
|
|
132
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
133
|
+
with:
|
|
134
|
+
repository-url: https://test.pypi.org/legacy/
|
|
135
|
+
|
|
136
|
+
pypi:
|
|
137
|
+
name: Publish to PyPI
|
|
138
|
+
runs-on: ubuntu-latest
|
|
139
|
+
needs: testpypi
|
|
140
|
+
environment: pypi
|
|
141
|
+
permissions:
|
|
142
|
+
id-token: write
|
|
143
|
+
|
|
144
|
+
steps:
|
|
145
|
+
- name: Download | Download Distribution Packages
|
|
146
|
+
uses: actions/download-artifact@v4
|
|
147
|
+
with:
|
|
148
|
+
name: logctx-distributions
|
|
149
|
+
path: dist/
|
|
150
|
+
|
|
151
|
+
- name: Publish | Upload package to PyPI
|
|
152
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
logctx-0.0.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Virtual environments
|
|
2
|
+
.venv/
|
|
3
|
+
__pypackages__/
|
|
4
|
+
|
|
5
|
+
# IDEs and editors
|
|
6
|
+
.vscode/
|
|
7
|
+
|
|
8
|
+
# Package distribution and build files
|
|
9
|
+
*.egg-info/
|
|
10
|
+
dist/
|
|
11
|
+
/build/
|
|
12
|
+
_build/
|
|
13
|
+
|
|
14
|
+
# Python bytecode and cache files
|
|
15
|
+
*.py[cod]
|
|
16
|
+
.cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
/.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# Benchmark and test files
|
|
22
|
+
.coverage
|
|
23
|
+
|
|
24
|
+
# Other files and folders
|
|
25
|
+
.python-version
|
|
26
|
+
/sandbox/
|
logctx-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Alexander Schulte.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
logctx-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logctx
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Management and injection of contextual variables into log messages.
|
|
5
|
+
Author: Alexander Schulte
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aschulte201/logctx
|
|
8
|
+
Project-URL: Source, https://github.com/aschulte201/logctx
|
|
9
|
+
Project-URL: Documentation, https://github.com/aschulte201/logctx/blob/README.md
|
|
10
|
+
Project-URL: Changelog, https://github.com/aschulte201/logctx/blob/CHANGELOG.md
|
|
11
|
+
Keywords: logging,context,log,logger,logctx,log-context
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: typing-extensions>=4.12
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
[](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml)
|
|
31
|
+
|
|
32
|
+
## Enabling
|
|
33
|
+
|
|
34
|
+
The core module provides a `logging.Filter` subclass designed to inject the current active context into any log messages.
|
|
35
|
+
|
|
36
|
+
Below is a demo usage on how to enable context injection:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import logging
|
|
40
|
+
import logctx
|
|
41
|
+
|
|
42
|
+
root_logger = logging.getLogger()
|
|
43
|
+
console_handler = logging.StreamHandler()
|
|
44
|
+
|
|
45
|
+
formatter = jsonlogger.JsonFormatter("%(logctx)s")
|
|
46
|
+
context_filter = ContextInjectingLoggingFilter(output_field="logctx")
|
|
47
|
+
|
|
48
|
+
console_handler.setFormatter(formatter)
|
|
49
|
+
console_handler.addFilter(context_filter)
|
|
50
|
+
|
|
51
|
+
root_logger.addHandler(console_handler)
|
|
52
|
+
logger.setLevel(logging.DEBUG)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
# Generators
|
|
56
|
+
* During execution
|
|
57
|
+
* Between yields
|
|
58
|
+
|
|
59
|
+
## Log Arguments
|
|
60
|
+
* Raises during initializtaion
|
|
61
|
+
* Value Error
|
|
62
|
+
* Able to rename args
|
|
63
|
+
* Unable to extract from kwargs
|
|
64
|
+
* Unable to work on generators
|
|
65
|
+
* Unable to work on async functions
|
|
66
|
+
|
|
67
|
+
# update
|
|
68
|
+
* can change root context
|
logctx-0.0.0/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml)
|
|
2
|
+
|
|
3
|
+
## Enabling
|
|
4
|
+
|
|
5
|
+
The core module provides a `logging.Filter` subclass designed to inject the current active context into any log messages.
|
|
6
|
+
|
|
7
|
+
Below is a demo usage on how to enable context injection:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import logging
|
|
11
|
+
import logctx
|
|
12
|
+
|
|
13
|
+
root_logger = logging.getLogger()
|
|
14
|
+
console_handler = logging.StreamHandler()
|
|
15
|
+
|
|
16
|
+
formatter = jsonlogger.JsonFormatter("%(logctx)s")
|
|
17
|
+
context_filter = ContextInjectingLoggingFilter(output_field="logctx")
|
|
18
|
+
|
|
19
|
+
console_handler.setFormatter(formatter)
|
|
20
|
+
console_handler.addFilter(context_filter)
|
|
21
|
+
|
|
22
|
+
root_logger.addHandler(console_handler)
|
|
23
|
+
logger.setLevel(logging.DEBUG)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
# Generators
|
|
27
|
+
* During execution
|
|
28
|
+
* Between yields
|
|
29
|
+
|
|
30
|
+
## Log Arguments
|
|
31
|
+
* Raises during initializtaion
|
|
32
|
+
* Value Error
|
|
33
|
+
* Able to rename args
|
|
34
|
+
* Unable to extract from kwargs
|
|
35
|
+
* Unable to work on generators
|
|
36
|
+
* Unable to work on async functions
|
|
37
|
+
|
|
38
|
+
# update
|
|
39
|
+
* can change root context
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""logctx package
|
|
2
|
+
|
|
3
|
+
This package provides a convenient way to manage logging contexts in Python.
|
|
4
|
+
|
|
5
|
+
It allows you to manage key-value pairs for log-contexts which can be automatically
|
|
6
|
+
added to log messages within their respective context.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__author__ = "Alexander Schulte"
|
|
10
|
+
__maintainer__ = "Alexander Schulte"
|
|
11
|
+
|
|
12
|
+
__version__ = "0.0.0"
|
|
13
|
+
|
|
14
|
+
from logctx import decorators
|
|
15
|
+
from logctx._core import (
|
|
16
|
+
ContextInjectingLoggingFilter,
|
|
17
|
+
LogContext,
|
|
18
|
+
clear,
|
|
19
|
+
get_current,
|
|
20
|
+
new_context,
|
|
21
|
+
update,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ContextInjectingLoggingFilter",
|
|
26
|
+
"LogContext",
|
|
27
|
+
"clear",
|
|
28
|
+
"get_current",
|
|
29
|
+
"new_context",
|
|
30
|
+
"update",
|
|
31
|
+
"decorators",
|
|
32
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
import dataclasses
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Generator, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
__all__: list[str] = [
|
|
8
|
+
"LogContext",
|
|
9
|
+
"get_current",
|
|
10
|
+
"new_context",
|
|
11
|
+
"update",
|
|
12
|
+
"clear",
|
|
13
|
+
"ContextInjectingLoggingFilter",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass(frozen=True)
|
|
18
|
+
class LogContext:
|
|
19
|
+
"""Dataclass holding information about one specific log context.
|
|
20
|
+
|
|
21
|
+
This class is used to store key-value pairs that are relevant for the
|
|
22
|
+
current logging context. It is designed to be immutable to prevent
|
|
23
|
+
accidental mutations by users.
|
|
24
|
+
|
|
25
|
+
If you want to update the context, use `logctx.update()` or `logctx.new_context()`.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
data (Mapping[str, Any]): A mapping of key-value pairs representing
|
|
29
|
+
the context data.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
data: Mapping[str, Any] = dataclasses.field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def with_values(self, **kwargs) -> "LogContext":
|
|
35
|
+
"""Create a new context with additional key-value pairs.
|
|
36
|
+
|
|
37
|
+
This method returns a new instance of LogContext with the current
|
|
38
|
+
context data merged with the provided key-value pairs. Duplicate keys
|
|
39
|
+
will be overwritten by the new values.
|
|
40
|
+
|
|
41
|
+
Caution:
|
|
42
|
+
This method does not affect the current active context, meaning that the
|
|
43
|
+
resulting context will not be included in any log messages.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
**kwargs: Key-value pairs to be added to the new context.
|
|
47
|
+
Returns:
|
|
48
|
+
LogContext: A new instance of LogContext with the merged data.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
return LogContext({**self.data, **kwargs})
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
"""Convert the context to a dictionary."""
|
|
55
|
+
|
|
56
|
+
return dict(self.data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_mdc_context: contextvars.ContextVar[LogContext] = contextvars.ContextVar("_mdc_context")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# TODO: return None or raise on no context
|
|
63
|
+
def get_current() -> LogContext:
|
|
64
|
+
"""Retrieve current context.
|
|
65
|
+
|
|
66
|
+
This function retrieves the current logging context from the context
|
|
67
|
+
variable. If no context is found, it returns an empty LogContext
|
|
68
|
+
instance.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
LogContext: The current logging context.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
return _mdc_context.get()
|
|
75
|
+
except LookupError:
|
|
76
|
+
return LogContext()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def new_context(**kwargs) -> Generator[LogContext, None, None]:
|
|
81
|
+
"""Create a new context with the provided key-value pairs.
|
|
82
|
+
|
|
83
|
+
The new context inherits all key-value pairs from the current context and
|
|
84
|
+
adds the provided pairs. Duplicate keys will be overwritten by the new values.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
**kwargs: Key-value pairs to be included in the new context.
|
|
88
|
+
|
|
89
|
+
Yields:
|
|
90
|
+
LogContext: The new logging context.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
current_log_ctx: LogContext = get_current()
|
|
94
|
+
new_log_ctx = current_log_ctx.with_values(**kwargs)
|
|
95
|
+
token = _mdc_context.set(new_log_ctx)
|
|
96
|
+
try:
|
|
97
|
+
yield new_log_ctx
|
|
98
|
+
finally:
|
|
99
|
+
_mdc_context.reset(token)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def update(**kwargs) -> LogContext:
|
|
103
|
+
"""Append key-value pairs to the current context.
|
|
104
|
+
|
|
105
|
+
Duplicate keys will be overwritten by the new values.
|
|
106
|
+
|
|
107
|
+
Will not affect log calls in current context made before the update.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
**kwargs: Key-value pairs to be added to the current context.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
LogContext: The updated logging context with the appended key-value
|
|
114
|
+
pairs.
|
|
115
|
+
"""
|
|
116
|
+
current_log_ctx = get_current()
|
|
117
|
+
updated_log_ctx = current_log_ctx.with_values(**kwargs)
|
|
118
|
+
_mdc_context.set(updated_log_ctx)
|
|
119
|
+
|
|
120
|
+
return updated_log_ctx
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def clear() -> None:
|
|
124
|
+
"""Clear the current context.
|
|
125
|
+
|
|
126
|
+
Only affects current context. After leaving current context, the context
|
|
127
|
+
will be reset to its previous state.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```python
|
|
131
|
+
with logctx.new_context(a=1, b=2):
|
|
132
|
+
with logctx.new_context(c=3):
|
|
133
|
+
# Context is now: {'a': 1, 'b': 2, 'c': 3}
|
|
134
|
+
logctx.clear()
|
|
135
|
+
# Context is now: {}
|
|
136
|
+
# Context is now: {'a': 1, 'b': 2}
|
|
137
|
+
```
|
|
138
|
+
"""
|
|
139
|
+
_mdc_context.set(LogContext())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ContextInjectingLoggingFilter(logging.Filter):
|
|
143
|
+
"""Logging filter that injects the current context into log records.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
name (str): The name of the filter. This is used to identify the
|
|
147
|
+
filter in the logging system.
|
|
148
|
+
|
|
149
|
+
output_field (str): The name of the field in the log record where the
|
|
150
|
+
context data will be injected. If not provided, the context data
|
|
151
|
+
will be injected into the log record as root level attributes.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, name: str = "", output_field: Optional[str] = None) -> None:
|
|
155
|
+
super().__init__(name=name)
|
|
156
|
+
self._output_field: Optional[str] = output_field
|
|
157
|
+
|
|
158
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
159
|
+
context: LogContext = get_current()
|
|
160
|
+
if self._output_field is not None:
|
|
161
|
+
setattr(record, self._output_field, context.to_dict())
|
|
162
|
+
else:
|
|
163
|
+
for k, v in context.to_dict().items():
|
|
164
|
+
setattr(record, k, v)
|
|
165
|
+
return True
|