slpy-log 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.
- slpy_log-0.1.0/LICENSE +146 -0
- slpy_log-0.1.0/PKG-INFO +529 -0
- slpy_log-0.1.0/README.md +489 -0
- slpy_log-0.1.0/pyproject.toml +49 -0
- slpy_log-0.1.0/setup.cfg +4 -0
- slpy_log-0.1.0/setup.py +69 -0
- slpy_log-0.1.0/slpy/__init__.py +49 -0
- slpy_log-0.1.0/slpy/config.py +85 -0
- slpy_log-0.1.0/slpy/context.py +47 -0
- slpy_log-0.1.0/slpy/core.py +140 -0
- slpy_log-0.1.0/slpy/fastapi.py +69 -0
- slpy_log-0.1.0/slpy/logger.py +122 -0
- slpy_log-0.1.0/slpy/masking.py +206 -0
- slpy_log-0.1.0/slpy/transport.py +218 -0
- slpy_log-0.1.0/slpy_log.egg-info/PKG-INFO +529 -0
- slpy_log-0.1.0/slpy_log.egg-info/SOURCES.txt +26 -0
- slpy_log-0.1.0/slpy_log.egg-info/dependency_links.txt +1 -0
- slpy_log-0.1.0/slpy_log.egg-info/not-zip-safe +1 -0
- slpy_log-0.1.0/slpy_log.egg-info/requires.txt +8 -0
- slpy_log-0.1.0/slpy_log.egg-info/top_level.txt +1 -0
- slpy_log-0.1.0/tests/test_adapter_transport.py +171 -0
- slpy_log-0.1.0/tests/test_config.py +40 -0
- slpy_log-0.1.0/tests/test_context.py +65 -0
- slpy_log-0.1.0/tests/test_core.py +126 -0
- slpy_log-0.1.0/tests/test_fastapi_middleware.py +135 -0
- slpy_log-0.1.0/tests/test_logger.py +171 -0
- slpy_log-0.1.0/tests/test_masking.py +93 -0
- slpy_log-0.1.0/tests/test_pretty_transport.py +121 -0
slpy_log-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship made available under
|
|
36
|
+
the License, as indicated by a copyright notice that is included in
|
|
37
|
+
or attached to the work (an example is provided in the Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other
|
|
42
|
+
transformations represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean, as defined by the copyright owner,
|
|
45
|
+
any work of authorship, including the original version of the Work
|
|
46
|
+
and any modifications or additions to that Work or Derivative Works
|
|
47
|
+
of the Work, that is intentionally submitted to the Licensor for
|
|
48
|
+
inclusion in the Work by or copyright owner or by an individual or
|
|
49
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
50
|
+
|
|
51
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
|
52
|
+
whom a Contribution has been received by the Licensor and included
|
|
53
|
+
within the Work.
|
|
54
|
+
|
|
55
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
56
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
57
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
58
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
59
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
60
|
+
Work and such Derivative Works in Source or Object form.
|
|
61
|
+
|
|
62
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
63
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
64
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
65
|
+
(except as stated in this section) patent license to make, have made,
|
|
66
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
67
|
+
where such license applies only to those patent contributions
|
|
68
|
+
Licensable by such Contributor that are necessarily infringed by
|
|
69
|
+
their Contribution(s) alone or by the combination of their
|
|
70
|
+
Contribution(s) with the Work to which such Contribution(s)
|
|
71
|
+
were submitted.
|
|
72
|
+
|
|
73
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
74
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
75
|
+
modifications, and in Source or Object form, provided that You
|
|
76
|
+
meet the following conditions:
|
|
77
|
+
|
|
78
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
79
|
+
Works a copy of the License; and
|
|
80
|
+
|
|
81
|
+
(b) You must cause any modified files to carry prominent notices
|
|
82
|
+
stating that You changed the files; and
|
|
83
|
+
|
|
84
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
85
|
+
that You distribute, all copyright, patent, trademark, and
|
|
86
|
+
attribution notices from the Source form of the Work; and
|
|
87
|
+
|
|
88
|
+
(d) If the Work includes a "NOTICE" text file, you must include a
|
|
89
|
+
readable copy of the attribution notices contained within such
|
|
90
|
+
NOTICE file, in all Source form Derivative Works that You
|
|
91
|
+
distribute.
|
|
92
|
+
|
|
93
|
+
You may add Your own attribution notices within Derivative Works
|
|
94
|
+
that You distribute, alongside or in addition to the NOTICE text
|
|
95
|
+
from the Work, provided that such additional attribution notices
|
|
96
|
+
cannot be construed as modifying the License.
|
|
97
|
+
|
|
98
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
99
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
100
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
101
|
+
this License, without any additional terms or conditions.
|
|
102
|
+
|
|
103
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
104
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
105
|
+
except as required for reasonable and customary use in describing the
|
|
106
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
107
|
+
|
|
108
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
109
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
110
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
111
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
112
|
+
implied. See the License for the specific language governing
|
|
113
|
+
permissions and limitations under the License.
|
|
114
|
+
|
|
115
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
116
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
117
|
+
unless required by applicable law (such as deliberate and grossly
|
|
118
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
119
|
+
liable to You for damages, including any direct, indirect, special,
|
|
120
|
+
incidental, or exemplary damages of any character arising as a
|
|
121
|
+
result of this License or out of the use or inability to use the
|
|
122
|
+
Work (even if such Contributor has been advised of the possibility
|
|
123
|
+
of such damages).
|
|
124
|
+
|
|
125
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
126
|
+
the Work or Derivative Works thereof, You may wish to offer, and
|
|
127
|
+
charge a fee for, acceptance of support, warranty, indemnity,
|
|
128
|
+
or other liability obligations and/or rights consistent with this
|
|
129
|
+
License. However, in accepting such obligations, You may offer only
|
|
130
|
+
conditions that are consistent with this terms of this License.
|
|
131
|
+
|
|
132
|
+
END OF TERMS AND CONDITIONS
|
|
133
|
+
|
|
134
|
+
Copyright 2026 Syntropysoft
|
|
135
|
+
|
|
136
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
137
|
+
you may not use this file except in compliance with the License.
|
|
138
|
+
You may obtain a copy of the License at
|
|
139
|
+
|
|
140
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
141
|
+
|
|
142
|
+
Unless required by applicable law or agreed to in writing, software
|
|
143
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
144
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
145
|
+
See the License for the specific language governing permissions and
|
|
146
|
+
limitations under the License.
|
slpy_log-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slpy-log
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Declarative Observability Framework for Python
|
|
5
|
+
Home-page: https://github.com/Syntropysoft/slpy
|
|
6
|
+
Author: Syntropysoft
|
|
7
|
+
Author-email: Syntropysoft <info@syntropysoft.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/Syntropysoft/slpy
|
|
10
|
+
Project-URL: Repository, https://github.com/Syntropysoft/slpy
|
|
11
|
+
Project-URL: Issues, https://github.com/Syntropysoft/slpy/issues
|
|
12
|
+
Keywords: logging,observability,masking,context,fastapi,async
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Logging
|
|
26
|
+
Classifier: Topic :: System :: Monitoring
|
|
27
|
+
Requires-Python: >=3.7
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="https://syntropysoft.com/syntropylog-logo.png" alt="SyntropyLog Logo" width="170"/>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h1 align="center">slpy</h1>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<strong>The Declarative Observability Framework for Python.</strong>
|
|
49
|
+
<br />
|
|
50
|
+
You declare what each log should carry. slpy handles the rest.
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="#"><img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Alpha"></a>
|
|
55
|
+
<a href="https://github.com/Syntropysoft/SyntropyLog/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
|
|
56
|
+
<a href="#"><img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="Python 3.7+"></a>
|
|
57
|
+
<a href="#"><img src="https://img.shields.io/badge/dependencies-zero-brightgreen.svg" alt="Zero dependencies"></a>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## What is slpy?
|
|
63
|
+
|
|
64
|
+
Every Python team writes the same boilerplate: thread `request_id` through every function signature, scrub `password` fields before logging, remember to call `logger.info()` instead of `print()`, repeat the same `extra={"service": "payment"}` on every call.
|
|
65
|
+
|
|
66
|
+
slpy solves the boilerplate problem declaratively. You declare the rules once at startup. The framework applies them consistently on every log call, in every coroutine, across every service — without you thinking about it again.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from slpy import slpy
|
|
70
|
+
|
|
71
|
+
await slpy.init({
|
|
72
|
+
'logger': {'level': 'info'},
|
|
73
|
+
'masking': {'enable_default_rules': True},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
logger = slpy.get_logger('payment-service')
|
|
77
|
+
|
|
78
|
+
async with slpy.context(request_id='req-001', user_id='usr-42'):
|
|
79
|
+
logger.info('Card charged', amount=299.90, email='john@example.com')
|
|
80
|
+
# → {"level":"info","message":"Card charged","service":"payment-service",
|
|
81
|
+
# "request_id":"req-001","user_id":"usr-42",
|
|
82
|
+
# "amount":299.9,"email":"j******n@example.com"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `request_id` propagated automatically. The `email` was masked automatically. The `service` field appeared automatically. You wrote none of that explicitly.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## The declarative shift
|
|
90
|
+
|
|
91
|
+
| Instead of... | You declare... | slpy does automatically |
|
|
92
|
+
|---------------|----------------|------------------------|
|
|
93
|
+
| Threading `request_id` through every function | `async with slpy.context(request_id=id)` | Propagates to all logs in scope via `contextvars` |
|
|
94
|
+
| Scrubbing sensitive fields before logging | `masking: {enable_default_rules: True}` | Masks email, password, token, credit card on every log |
|
|
95
|
+
| Repeating `extra={"service": "payment"}` | `slpy.get_logger('payment-service')` | `service` field on every log from that logger |
|
|
96
|
+
| Copying context into child functions | `logger.child(order_id='123')` | All bindings carried automatically on every subsequent call |
|
|
97
|
+
| Routing compliance logs manually | `logger.with_meta({"regulation": "GDPR"})` | `meta` payload travels sanitized to all transports |
|
|
98
|
+
| Writing a transport class per destination | `AdapterTransport(adapter=UniversalAdapter(executor=fn))` | Your executor receives the clean entry — connect to anything |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Quick start
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install slpy
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import asyncio
|
|
110
|
+
from slpy import slpy
|
|
111
|
+
|
|
112
|
+
async def main():
|
|
113
|
+
await slpy.init({
|
|
114
|
+
'logger': {'level': 'info'},
|
|
115
|
+
'masking': {'enable_default_rules': True},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
logger = slpy.get_logger('my-service')
|
|
119
|
+
logger.info('Service started')
|
|
120
|
+
|
|
121
|
+
await slpy.shutdown()
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
No transport configured → `ConsoleTransport` (structured JSON) is used automatically.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Named loggers and child()
|
|
131
|
+
|
|
132
|
+
Each component gets its own named logger. `child()` binds context once — every log from that instance carries it automatically. Bindings are immutable and composable.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
async def process_order(order_id: str, user_id: str) -> None:
|
|
136
|
+
# Bind once — no need to repeat on every call
|
|
137
|
+
logger = slpy.get_logger('order-service').child(
|
|
138
|
+
order_id=order_id,
|
|
139
|
+
user_id=user_id,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
logger.info('Processing') # carries order_id, user_id
|
|
143
|
+
logger.info('Calculated', total=299.90, items=3) # carries order_id, user_id
|
|
144
|
+
|
|
145
|
+
payment = logger.child(step='payment') # adds step, keeps the rest
|
|
146
|
+
payment.info('Charging card') # carries order_id, user_id, step
|
|
147
|
+
payment.info('Approved', amount=299.90)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`child()` never mutates the parent. Each call returns a new logger with merged bindings.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Context propagation
|
|
155
|
+
|
|
156
|
+
slpy uses Python's native `contextvars` — the same mechanism as `AsyncLocalStorage` in Node.js. Context propagates correctly across `asyncio.gather()`, `asyncio.create_task()`, and thread-pool executors.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
async def handle_request(request_id: str, user_id: str) -> None:
|
|
160
|
+
async with slpy.context(request_id=request_id, user_id=user_id):
|
|
161
|
+
logger.info('Request received') # request_id, user_id here
|
|
162
|
+
await fetch_from_db() # request_id, user_id here too
|
|
163
|
+
logger.info('Request complete')
|
|
164
|
+
|
|
165
|
+
async def fetch_from_db() -> None:
|
|
166
|
+
# No function argument needed — context is already here
|
|
167
|
+
logger.debug('Running query') # request_id, user_id propagated
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Concurrent requests are fully isolated. Each `async with slpy.context(...)` opens its own scope; inner scopes do not leak into outer ones.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Data masking
|
|
175
|
+
|
|
176
|
+
Masking runs automatically on every log entry. Default rules cover the most common sensitive fields. The engine flattens nested objects, applies rules by field name, then reconstructs the original structure — at any depth.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
await slpy.init({
|
|
180
|
+
'masking': {
|
|
181
|
+
'enable_default_rules': True, # email, password, token, credit card, SSN, phone
|
|
182
|
+
'rules': [
|
|
183
|
+
# Add your own — patterns compiled once at init()
|
|
184
|
+
{'pattern': r'internal_code', 'strategy': 'token'},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
logger.info('Payment', credit_card_number='4111-1111-1111-1234', amount=299.90)
|
|
190
|
+
# → credit_card_number: "************1234" amount: 299.9 (not masked)
|
|
191
|
+
|
|
192
|
+
logger.info('User', email='john@example.com', name='John Doe')
|
|
193
|
+
# → email: "j******n@example.com" name: "John Doe" (not masked)
|
|
194
|
+
|
|
195
|
+
logger.info('Order', order={'user': {'token': 'abc123', 'id': 'USR-1'}})
|
|
196
|
+
# → order.user.token: "******" order.user.id: "USR-1" (not masked)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Default rules**
|
|
200
|
+
|
|
201
|
+
| Field pattern | Strategy | Example result |
|
|
202
|
+
|---------------|----------|----------------|
|
|
203
|
+
| `email`, `mail` | Email | `j******n@example.com` |
|
|
204
|
+
| `password`, `pass`, `pwd`, `secret` | Full mask | `************` |
|
|
205
|
+
| `token`, `key`, `auth`, `jwt`, `bearer` | Full mask | `**********` |
|
|
206
|
+
| `credit_card`, `card_number` | Last 4 | `************1234` |
|
|
207
|
+
| `ssn`, `social_security` | Last 4 | `*****6789` |
|
|
208
|
+
| `phone`, `mobile`, `tel` | Last 4 | `*******4567` |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Audit level and with_meta()
|
|
213
|
+
|
|
214
|
+
`audit` bypasses the configured level filter — it is always emitted. Use it for compliance events that must always be recorded regardless of the runtime log level.
|
|
215
|
+
|
|
216
|
+
`with_meta(payload)` attaches arbitrary structured metadata to every log from that logger instance. The payload travels sanitized to all transports as `logEntry['meta']`.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
audit_logger = (
|
|
220
|
+
slpy.get_logger('compliance')
|
|
221
|
+
.with_meta({
|
|
222
|
+
'ttl_days': 730,
|
|
223
|
+
'regulation': 'GDPR',
|
|
224
|
+
'data_class': 'PII',
|
|
225
|
+
'destination': 'audit-store',
|
|
226
|
+
})
|
|
227
|
+
.child(user_id='USR-42')
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
audit_logger.audit('Data exported', records=1500)
|
|
231
|
+
# → level: "audit" meta: {ttl_days: 730, regulation: "GDPR", ...}
|
|
232
|
+
# user_id: "USR-42" records: 1500
|
|
233
|
+
|
|
234
|
+
# audit always appears — even when level is 'error'
|
|
235
|
+
await slpy.init({'logger': {'level': 'error'}})
|
|
236
|
+
logger.info('hidden') # not emitted
|
|
237
|
+
logger.audit('visible') # always emitted
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**with_meta() use cases**
|
|
241
|
+
|
|
242
|
+
| Use case | Payload |
|
|
243
|
+
|----------|---------|
|
|
244
|
+
| Log retention routing | `{"ttl_days": 730, "destination": "cold-storage"}` |
|
|
245
|
+
| Compliance tagging | `{"regulation": "GDPR", "data_class": "PII"}` |
|
|
246
|
+
| Experiment tracking | `{"experiment": "checkout-v2", "variant": "B"}` |
|
|
247
|
+
| Release context | `{"version": "1.4.0", "deploy_id": "d-001"}` |
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## FastAPI / ASGI middleware
|
|
252
|
+
|
|
253
|
+
One line wires automatic context propagation into every request.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
pip install slpy[fastapi]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from fastapi import FastAPI
|
|
261
|
+
from slpy import slpy
|
|
262
|
+
from slpy.fastapi import SyntropyMiddleware
|
|
263
|
+
|
|
264
|
+
app = FastAPI()
|
|
265
|
+
app.add_middleware(SyntropyMiddleware)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Every log emitted during a request automatically carries `correlation_id`, `method`, and `path` — extracted from the incoming header or generated if absent. The `X-Correlation-ID` header is forwarded in the response.
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
@app.get('/orders/{order_id}')
|
|
272
|
+
async def get_order(order_id: str):
|
|
273
|
+
logger.info('Fetching', order_id=order_id)
|
|
274
|
+
# → {"level":"info","message":"Fetching","service":"api",
|
|
275
|
+
# "correlation_id":"req-abc","method":"GET","path":"/orders/123",
|
|
276
|
+
# "order_id":"123"}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`SyntropyMiddleware` is pure ASGI — it works with FastAPI, Starlette, Litestar, and any ASGI server (uvicorn, hypercorn) without framework-specific dependencies.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Runtime level change
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
sl.set_level('debug') # all existing loggers update immediately
|
|
287
|
+
sl.set_level('error') # back to quiet
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Transports
|
|
293
|
+
|
|
294
|
+
| Transport | Output | Use case |
|
|
295
|
+
|-----------|--------|----------|
|
|
296
|
+
| `ConsoleTransport` | Structured JSON | Production, CI, log collectors — **default** |
|
|
297
|
+
| `PrettyConsoleTransport` | Colored human-readable | Local development |
|
|
298
|
+
| `AdapterTransport` | Any destination | Databases, HTTP APIs, queues, multiple targets |
|
|
299
|
+
|
|
300
|
+
### AdapterTransport + UniversalAdapter
|
|
301
|
+
|
|
302
|
+
The most powerful routing primitive. You provide an `executor` function — sync or async — that receives the clean, already-masked log entry and sends it anywhere. slpy handles context propagation, masking, level filtering, error isolation, and fanout.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from slpy import slpy, AdapterTransport, UniversalAdapter, ConsoleTransport
|
|
306
|
+
|
|
307
|
+
async def my_executor(data: dict) -> None:
|
|
308
|
+
# One function. Any number of destinations. Fully async.
|
|
309
|
+
await asyncio.gather(
|
|
310
|
+
prisma.system_log.create(data=data),
|
|
311
|
+
mongo_collection.insert_one(data),
|
|
312
|
+
es.index(index='logs', body=data),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
db_transport = AdapterTransport(
|
|
316
|
+
name='db',
|
|
317
|
+
adapter=UniversalAdapter(executor=my_executor),
|
|
318
|
+
formatter=lambda e: {**e, 'timestamp': datetime.fromisoformat(e['timestamp'])},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
await slpy.init({
|
|
322
|
+
'logger': {
|
|
323
|
+
'transports': [
|
|
324
|
+
db_transport, # → Postgres + MongoDB + Elasticsearch
|
|
325
|
+
ConsoleTransport(), # → stdout
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
'masking': {'enable_default_rules': True},
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
When `my_executor` is called, the entry is already:
|
|
333
|
+
- **Masked** — `email`, `password`, `token`, credit card fields scrubbed
|
|
334
|
+
- **Context-enriched** — `request_id`, `user_id`, any `slpy.context()` fields attached
|
|
335
|
+
- **Formatted** — optionally transformed by your `formatter` to match your DB schema
|
|
336
|
+
|
|
337
|
+
The executor is the only thing you write. Connect to Postgres, MongoDB, Elasticsearch, Datadog, OpenTelemetry, Kafka, a REST API, or all of them at once — slpy does not know or care.
|
|
338
|
+
|
|
339
|
+
**`formatter`** (optional) — transform the entry before it reaches the executor. Use it to map field names, convert types, or add schema-specific fields:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
def db_formatter(entry: dict) -> dict:
|
|
343
|
+
return {
|
|
344
|
+
**entry,
|
|
345
|
+
'timestamp': datetime.fromtimestamp(entry['timestamp'] / 1000),
|
|
346
|
+
'env': os.getenv('APP_ENV', 'production'),
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### PrettyConsoleTransport
|
|
351
|
+
|
|
352
|
+
Human-readable colored output. Zero external dependencies — pure ANSI codes.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from slpy import slpy, PrettyConsoleTransport
|
|
356
|
+
|
|
357
|
+
await slpy.init({
|
|
358
|
+
'logger': {
|
|
359
|
+
'level': 'debug',
|
|
360
|
+
'transports': [PrettyConsoleTransport()],
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
12:00:00.123 DEBUG payment-service Starting up version=0.1.0
|
|
367
|
+
12:00:00.124 INFO payment-service Service ready port=8080
|
|
368
|
+
12:00:00.124 WARN payment-service High memory usage heap_mb=420
|
|
369
|
+
12:00:00.124 ERROR payment-service Connection refused host=db.internal
|
|
370
|
+
12:00:00.124 INFO payment-service User login email=j**n@example.com
|
|
371
|
+
12:00:00.124 AUDIT payment-service Data exported meta={"regulation":"GDPR"}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Auto-detects TTY** — when stdout is not a terminal (CI, pipes, production), falls back to JSON
|
|
375
|
+
automatically. No code change needed between environments.
|
|
376
|
+
|
|
377
|
+
Masking remains active in pretty output — sensitive fields are masked regardless of transport.
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
PrettyConsoleTransport() # auto-detect TTY (recommended)
|
|
381
|
+
PrettyConsoleTransport(colors=True) # force colors on
|
|
382
|
+
PrettyConsoleTransport(colors=False)# force JSON (same as ConsoleTransport)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Custom transports
|
|
386
|
+
|
|
387
|
+
For full control, extend `Transport` and implement `log(entry)`:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from slpy.transport import Transport
|
|
391
|
+
|
|
392
|
+
class ElasticsearchTransport(Transport):
|
|
393
|
+
def log(self, entry: dict) -> None:
|
|
394
|
+
self._es_client.index(index='logs', body=entry)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
For most cases, `AdapterTransport` + `UniversalAdapter` is simpler — no subclassing needed.
|
|
398
|
+
|
|
399
|
+
Multiple transports are supported — entries are sent to all of them.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Performance
|
|
404
|
+
|
|
405
|
+
slpy includes an optional Rust addon (`slpy-native`) that accelerates the masking engine. When installed, it is used automatically with no code changes.
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
Simple log Complex object + masking
|
|
409
|
+
--------------------- ------------------------------------
|
|
410
|
+
slpy 6.2 µs ① structlog 10.8 µs (no masking)
|
|
411
|
+
structlog 8.0 µs slpy 18.0 µs ② ✅ masking ON
|
|
412
|
+
logging 18.8 µs logging 20.4 µs (no masking)
|
|
413
|
+
loguru 58.9 µs loguru 61.8 µs (no masking)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
> pyperf, null transport, Windows 11 local — numbers above are conservative.
|
|
417
|
+
|
|
418
|
+
**slpy is the fastest structured logger on simple logs** — faster than structlog, which is the Python performance reference.
|
|
419
|
+
|
|
420
|
+
**slpy with masking fully active is faster than the stdlib `logging` module without masking.** It is 3.4x faster than loguru without masking.
|
|
421
|
+
|
|
422
|
+
The Rust addon reduced masking overhead from 29 µs to 12 µs (59% improvement). If the addon is not installed, slpy falls back to the pure Python engine transparently.
|
|
423
|
+
|
|
424
|
+
### Sustained throughput — official results (GitHub Actions, Ubuntu, Python 3.12)
|
|
425
|
+
|
|
426
|
+
| Scenario | logs/sec | µs/log | degradation |
|
|
427
|
+
|----------|----------|--------|-------------|
|
|
428
|
+
| Simple log | **85,774** | 11.7 | — |
|
|
429
|
+
| MaskingEngine only | **59,369** | 16.8 | — |
|
|
430
|
+
| child() + log | **58,165** | 17.2 | 0 B heap |
|
|
431
|
+
| Complex log + masking | **33,472** | 29.9 | — |
|
|
432
|
+
| Async context scope | **22,237** | 45.0 | none (1M → 10M: 44.97 → 44.76 µs) |
|
|
433
|
+
|
|
434
|
+
Zero degradation across all scenarios. `child()` and async context scope allocate **0 bytes** at sustained volume — no GC pressure regardless of call volume.
|
|
435
|
+
|
|
436
|
+
See [benchmark/README.md](benchmark/README.md) for full methodology and how to reproduce.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## What slpy is not
|
|
441
|
+
|
|
442
|
+
slpy is a structured logging and context propagation framework. It is not:
|
|
443
|
+
|
|
444
|
+
- A log aggregation backend (use Elasticsearch, Loki, CloudWatch)
|
|
445
|
+
- A distributed tracing system (use OpenTelemetry)
|
|
446
|
+
- A metrics collector (use Prometheus, Datadog)
|
|
447
|
+
|
|
448
|
+
It is the component that makes every log line correct, consistent, and safe before it reaches any of those systems.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Security
|
|
453
|
+
|
|
454
|
+
**No network I/O at runtime.** slpy does not contact any external URLs. The only output is what your transports produce.
|
|
455
|
+
|
|
456
|
+
**Zero runtime dependencies.** The core package has no `install_requires`. The optional `[fastapi]` extra adds only `starlette` (already a FastAPI dependency).
|
|
457
|
+
|
|
458
|
+
**Masking and custom functions.** The `custom_mask` function in masking rules is consumer-supplied configuration — it is not influenced by external input. See [socket.dev note](#) for full analysis.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Installation
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# Core (zero dependencies)
|
|
466
|
+
pip install slpy
|
|
467
|
+
|
|
468
|
+
# With FastAPI middleware
|
|
469
|
+
pip install slpy[fastapi]
|
|
470
|
+
|
|
471
|
+
# Development
|
|
472
|
+
pip install slpy[dev]
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Requires Python 3.7+.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Running the examples
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
git clone https://github.com/Syntropysoft/slpy
|
|
483
|
+
cd slpy
|
|
484
|
+
|
|
485
|
+
# Basic setup
|
|
486
|
+
py examples/01_basic_setup.py
|
|
487
|
+
|
|
488
|
+
# Named loggers and child()
|
|
489
|
+
py examples/02_named_loggers_and_child.py
|
|
490
|
+
|
|
491
|
+
# Context propagation with asyncio.gather()
|
|
492
|
+
py examples/03_context_propagation.py
|
|
493
|
+
|
|
494
|
+
# Data masking
|
|
495
|
+
py examples/04_masking.py
|
|
496
|
+
|
|
497
|
+
# with_meta() and audit level
|
|
498
|
+
py examples/05_with_meta_and_audit.py
|
|
499
|
+
|
|
500
|
+
# FastAPI middleware (requires: pip install fastapi uvicorn)
|
|
501
|
+
uvicorn examples.06_fastapi_middleware:app --reload
|
|
502
|
+
|
|
503
|
+
# PrettyConsoleTransport — colored output (run in a real terminal)
|
|
504
|
+
py examples/07_pretty_console.py
|
|
505
|
+
|
|
506
|
+
# AdapterTransport + UniversalAdapter — fanout to multiple destinations
|
|
507
|
+
py examples/08_adapter_transport.py
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Running the tests
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
pip install slpy[dev]
|
|
514
|
+
pytest
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Documentation
|
|
520
|
+
|
|
521
|
+
- **[Refactor plan and architecture](docs/slpy-refactor-plan.md)** — Design decisions and what was removed and why.
|
|
522
|
+
- **[Data masking](docs/data_masking.md)** — Masking strategies, default rules, custom rules.
|
|
523
|
+
- **[Logger matrix](docs/logger_matrix.md)** — Per-level field filtering.
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## License
|
|
528
|
+
|
|
529
|
+
Apache 2.0 — see [LICENSE](./LICENSE).
|