mobius-logging-py 1.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.
- mobius_logging_py-1.0.0/PKG-INFO +211 -0
- mobius_logging_py-1.0.0/README.md +192 -0
- mobius_logging_py-1.0.0/pyproject.toml +38 -0
- mobius_logging_py-1.0.0/setup.cfg +4 -0
- mobius_logging_py-1.0.0/src/mobius_logging/__init__.py +80 -0
- mobius_logging_py-1.0.0/src/mobius_logging/bootstrap.py +120 -0
- mobius_logging_py-1.0.0/src/mobius_logging/constants.py +32 -0
- mobius_logging_py-1.0.0/src/mobius_logging/context.py +140 -0
- mobius_logging_py-1.0.0/src/mobius_logging/fields.py +83 -0
- mobius_logging_py-1.0.0/src/mobius_logging/kafka_context.py +57 -0
- mobius_logging_py-1.0.0/src/mobius_logging/kafka_producer.py +127 -0
- mobius_logging_py-1.0.0/src/mobius_logging/log_type.py +20 -0
- mobius_logging_py-1.0.0/src/mobius_logging/logging_config.py +122 -0
- mobius_logging_py-1.0.0/src/mobius_logging/masking.py +78 -0
- mobius_logging_py-1.0.0/src/mobius_logging/middleware.py +65 -0
- mobius_logging_py-1.0.0/src/mobius_logging/observed.py +115 -0
- mobius_logging_py-1.0.0/src/mobius_logging/py.typed +0 -0
- mobius_logging_py-1.0.0/src/mobius_logging_py.egg-info/PKG-INFO +211 -0
- mobius_logging_py-1.0.0/src/mobius_logging_py.egg-info/SOURCES.txt +22 -0
- mobius_logging_py-1.0.0/src/mobius_logging_py.egg-info/dependency_links.txt +1 -0
- mobius_logging_py-1.0.0/src/mobius_logging_py.egg-info/requires.txt +13 -0
- mobius_logging_py-1.0.0/src/mobius_logging_py.egg-info/top_level.txt +1 -0
- mobius_logging_py-1.0.0/tests/test_core.py +139 -0
- mobius_logging_py-1.0.0/tests/test_middleware.py +35 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobius-logging-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Standardized logging context, observation, masking and Kafka schema events for Mobius Python (FastAPI) services.
|
|
5
|
+
Author: Mobius Platform
|
|
6
|
+
Keywords: logging,observability,fastapi,mobius,kafka
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: py-kafka-producer-client>=0.1.7
|
|
10
|
+
Provides-Extra: otel
|
|
11
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
|
|
12
|
+
Provides-Extra: fastapi
|
|
13
|
+
Requires-Dist: starlette>=0.27; extra == "fastapi"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
16
|
+
Requires-Dist: starlette>=0.27; extra == "dev"
|
|
17
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
18
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Mobius Logging for Python
|
|
21
|
+
|
|
22
|
+
Python port of the Java Mobius logging library. It standardizes logging context
|
|
23
|
+
for Mobius **FastAPI / Starlette** services: a contextvars-based context store
|
|
24
|
+
(the analog of SLF4J MDC), a request middleware, an `@observed` method
|
|
25
|
+
decorator, sensitive-data masking, OpenTelemetry trace sync, JSON logging, and
|
|
26
|
+
a Kafka schema-event producer.
|
|
27
|
+
|
|
28
|
+
Schema constants (Kafka topic `construct-data-1`, schema id, tenant id, and
|
|
29
|
+
`schema_version=mobius.log.v1`) are **identical to the Java library**, so events
|
|
30
|
+
from Python services land in the same pipeline.
|
|
31
|
+
|
|
32
|
+
Requires Python 3.10+.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install mobius-logging-py
|
|
38
|
+
|
|
39
|
+
# or directly from git
|
|
40
|
+
pip install "mobius-logging-py @ git+https://<your-git-host>/mobius-logging-py.git@v1.0.0"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The import package is `mobius_logging` (e.g. `from mobius_logging import setup_logging`).
|
|
44
|
+
|
|
45
|
+
Optional extras: `mobius-logging-py[otel]` (OpenTelemetry trace sync),
|
|
46
|
+
`mobius-logging-py[fastapi]` (Starlette middleware).
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi import FastAPI
|
|
52
|
+
from mobius_logging import setup_logging, observed, LogType
|
|
53
|
+
from py_kafka_producer_client import Producer # your internal client
|
|
54
|
+
|
|
55
|
+
app = FastAPI()
|
|
56
|
+
|
|
57
|
+
setup_logging(
|
|
58
|
+
app,
|
|
59
|
+
service_name="orders-service",
|
|
60
|
+
profile="prod", # json logs for staging/prod/production
|
|
61
|
+
sensitive_keys=["session_id", "refresh_token"],
|
|
62
|
+
kafka_producer=Producer(...), # wrapped in PyKafkaProducerClientSender
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@app.post("/orders")
|
|
66
|
+
@observed(event_type="order.create", log_type=LogType.AUDIT, resource_type="order")
|
|
67
|
+
async def create_order(order_id: str):
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`setup_logging` is the one-call equivalent of Java's Spring auto-configuration
|
|
72
|
+
(Python has no auto-config, so wiring is explicit). It configures logging,
|
|
73
|
+
registers custom sensitive keys, builds the schema producer, and adds the
|
|
74
|
+
request middleware.
|
|
75
|
+
|
|
76
|
+
## Java → Python mapping
|
|
77
|
+
|
|
78
|
+
| Java Mobius logging library | Python (`mobius_logging`) |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| SLF4J `MDC` | `contextvars.ContextVar` (`context.py`) |
|
|
81
|
+
| `PlatformLogContext` | `mobius_logging.context` functions |
|
|
82
|
+
| `PlatformLogFields` | `mobius_logging.fields` |
|
|
83
|
+
| `LogType` enum | `LogType` enum |
|
|
84
|
+
| `PlatformLoggingFilter` (servlet) | `PlatformLoggingMiddleware` (ASGI) |
|
|
85
|
+
| `@PlatformObserved` + AOP aspect | `@observed` decorator |
|
|
86
|
+
| `KafkaLogProducer` (`KafkaTemplate`) | `KafkaLogProducer` + `LogEventSender` |
|
|
87
|
+
| `PlatformKafka*Context` | `producer_headers` / `apply_consumer_record` |
|
|
88
|
+
| `SensitiveDataMasker` | `mobius_logging.masking` |
|
|
89
|
+
| `LogbackConfigurator` | `configure_platform_logging` |
|
|
90
|
+
| Spring auto-config classes | `setup_logging` (explicit) |
|
|
91
|
+
|
|
92
|
+
Out of scope for v1 (present in Java): JDBC/JPA, MongoDB, and Camunda logging.
|
|
93
|
+
|
|
94
|
+
## Context API
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from mobius_logging import context, LogType
|
|
98
|
+
|
|
99
|
+
context.init() # sets schema_version
|
|
100
|
+
context.tenant_id("tenant-123")
|
|
101
|
+
context.log_type(LogType.AUDIT)
|
|
102
|
+
context.resource_type("order")
|
|
103
|
+
context.resource_id("order-789")
|
|
104
|
+
context.field("custom_key", "value")
|
|
105
|
+
context.ensure_correlation_id() # generates one if missing
|
|
106
|
+
context.sync_trace_from_opentelemetry()
|
|
107
|
+
|
|
108
|
+
snap = context.snapshot()
|
|
109
|
+
try:
|
|
110
|
+
context.put("event_type", "order.approved")
|
|
111
|
+
finally:
|
|
112
|
+
context.restore(snap)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> Note: Java's `ensureCorrelationId()` has an inverted condition and only
|
|
116
|
+
> regenerates when one already exists. This port fixes that — it generates a
|
|
117
|
+
> correlation id when none is present.
|
|
118
|
+
|
|
119
|
+
## `@observed`
|
|
120
|
+
|
|
121
|
+
Works on sync and async functions. Adds `log_type`, `event_type`,
|
|
122
|
+
`resource_type`, `class_name`, `method_name`, `outcome`, `method_duration_ms`,
|
|
123
|
+
and `exception_type`/`exception_message` on failure. After completion it ships
|
|
124
|
+
the context to the schema producer (if one is registered), then restores the
|
|
125
|
+
pre-call context snapshot (Java clears MDC instead).
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
@observed(event_type="order.create", resource_type="order")
|
|
129
|
+
def create_order(order_id: str): ...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Kafka
|
|
133
|
+
|
|
134
|
+
### Schema event producer
|
|
135
|
+
|
|
136
|
+
The producer depends only on a small protocol, so the library has no hard Kafka
|
|
137
|
+
dependency. It matches the `py-kafka-producer-client` (>=0.1.7) signature — the
|
|
138
|
+
event is a dict and `topic` is keyword-only:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
class LogEventSender(Protocol):
|
|
142
|
+
def send(self, value: dict, *, topic: str) -> Any: ...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For the internal `py-kafka-producer-client`, pass the client to
|
|
146
|
+
`setup_logging(kafka_producer=...)` or wrap it directly:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from mobius_logging import KafkaLogProducer, PyKafkaProducerClientSender, set_log_producer
|
|
150
|
+
|
|
151
|
+
producer = KafkaLogProducer(PyKafkaProducerClientSender(client), "orders-service")
|
|
152
|
+
set_log_producer(producer)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`PyKafkaProducerClientSender` calls `client.send(value, topic=...)`, passing the
|
|
156
|
+
`DataIngestionOperation` envelope as a dict (the client serializes it). To use a
|
|
157
|
+
different client, pass any object exposing `send(value, *, topic)` as
|
|
158
|
+
`kafka_sender=`.
|
|
159
|
+
|
|
160
|
+
Sending runs on a background thread pool (best-effort, never raises, never
|
|
161
|
+
blocks the request) — the analog of the Java `@Async` method.
|
|
162
|
+
|
|
163
|
+
### Context propagation
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from mobius_logging import producer_headers, apply_consumer_record
|
|
167
|
+
|
|
168
|
+
# producer side
|
|
169
|
+
client.send(topic, value, headers=producer_headers())
|
|
170
|
+
|
|
171
|
+
# consumer side
|
|
172
|
+
apply_consumer_record(record.headers(), topic=record.topic(),
|
|
173
|
+
partition=record.partition(), offset=record.offset())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Propagated header fields: `trace_id`, `span_id`, `correlation_id`,
|
|
177
|
+
`causation_id`, `tenant_id`. Consumer also sets `kafka_topic`,
|
|
178
|
+
`kafka_partition`, `kafka_offset`.
|
|
179
|
+
|
|
180
|
+
## Logging output
|
|
181
|
+
|
|
182
|
+
`configure_platform_logging(service_name, profile)` installs a root handler that
|
|
183
|
+
injects the current context onto every record. Profiles `staging`/`prod`/
|
|
184
|
+
`production` emit JSON; others use a readable text pattern. A daily-rotating
|
|
185
|
+
file handler writes to `logs/<service>.log` (30 days retained).
|
|
186
|
+
|
|
187
|
+
In JSON mode every context field is included automatically. For the text
|
|
188
|
+
pattern, `txn_id`, `tenant_id`, and `trace_id` are surfaced inline.
|
|
189
|
+
|
|
190
|
+
## Sensitive data masking
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from mobius_logging import mask, mask_map, init_custom_keys
|
|
194
|
+
|
|
195
|
+
init_custom_keys(["session_id", "refresh_token"])
|
|
196
|
+
mask("authorization", token) # masked
|
|
197
|
+
mask_map(context.copy()) # masks all sensitive keys in a dict
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Default sensitive keys: `password`, `token`, `secret`, `authorization`,
|
|
201
|
+
`cookie`, `apikey`, `privatekey`, `kubeconfig`. Matching is case-insensitive.
|
|
202
|
+
Masking keeps the last 4 characters of generic values and partially masks
|
|
203
|
+
emails — identical behaviour to the Java masker.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python -m venv .venv && source .venv/bin/activate
|
|
209
|
+
pip install -e ".[dev]"
|
|
210
|
+
pytest
|
|
211
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Mobius Logging for Python
|
|
2
|
+
|
|
3
|
+
Python port of the Java Mobius logging library. It standardizes logging context
|
|
4
|
+
for Mobius **FastAPI / Starlette** services: a contextvars-based context store
|
|
5
|
+
(the analog of SLF4J MDC), a request middleware, an `@observed` method
|
|
6
|
+
decorator, sensitive-data masking, OpenTelemetry trace sync, JSON logging, and
|
|
7
|
+
a Kafka schema-event producer.
|
|
8
|
+
|
|
9
|
+
Schema constants (Kafka topic `construct-data-1`, schema id, tenant id, and
|
|
10
|
+
`schema_version=mobius.log.v1`) are **identical to the Java library**, so events
|
|
11
|
+
from Python services land in the same pipeline.
|
|
12
|
+
|
|
13
|
+
Requires Python 3.10+.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install mobius-logging-py
|
|
19
|
+
|
|
20
|
+
# or directly from git
|
|
21
|
+
pip install "mobius-logging-py @ git+https://<your-git-host>/mobius-logging-py.git@v1.0.0"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The import package is `mobius_logging` (e.g. `from mobius_logging import setup_logging`).
|
|
25
|
+
|
|
26
|
+
Optional extras: `mobius-logging-py[otel]` (OpenTelemetry trace sync),
|
|
27
|
+
`mobius-logging-py[fastapi]` (Starlette middleware).
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from fastapi import FastAPI
|
|
33
|
+
from mobius_logging import setup_logging, observed, LogType
|
|
34
|
+
from py_kafka_producer_client import Producer # your internal client
|
|
35
|
+
|
|
36
|
+
app = FastAPI()
|
|
37
|
+
|
|
38
|
+
setup_logging(
|
|
39
|
+
app,
|
|
40
|
+
service_name="orders-service",
|
|
41
|
+
profile="prod", # json logs for staging/prod/production
|
|
42
|
+
sensitive_keys=["session_id", "refresh_token"],
|
|
43
|
+
kafka_producer=Producer(...), # wrapped in PyKafkaProducerClientSender
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@app.post("/orders")
|
|
47
|
+
@observed(event_type="order.create", log_type=LogType.AUDIT, resource_type="order")
|
|
48
|
+
async def create_order(order_id: str):
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`setup_logging` is the one-call equivalent of Java's Spring auto-configuration
|
|
53
|
+
(Python has no auto-config, so wiring is explicit). It configures logging,
|
|
54
|
+
registers custom sensitive keys, builds the schema producer, and adds the
|
|
55
|
+
request middleware.
|
|
56
|
+
|
|
57
|
+
## Java → Python mapping
|
|
58
|
+
|
|
59
|
+
| Java Mobius logging library | Python (`mobius_logging`) |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| SLF4J `MDC` | `contextvars.ContextVar` (`context.py`) |
|
|
62
|
+
| `PlatformLogContext` | `mobius_logging.context` functions |
|
|
63
|
+
| `PlatformLogFields` | `mobius_logging.fields` |
|
|
64
|
+
| `LogType` enum | `LogType` enum |
|
|
65
|
+
| `PlatformLoggingFilter` (servlet) | `PlatformLoggingMiddleware` (ASGI) |
|
|
66
|
+
| `@PlatformObserved` + AOP aspect | `@observed` decorator |
|
|
67
|
+
| `KafkaLogProducer` (`KafkaTemplate`) | `KafkaLogProducer` + `LogEventSender` |
|
|
68
|
+
| `PlatformKafka*Context` | `producer_headers` / `apply_consumer_record` |
|
|
69
|
+
| `SensitiveDataMasker` | `mobius_logging.masking` |
|
|
70
|
+
| `LogbackConfigurator` | `configure_platform_logging` |
|
|
71
|
+
| Spring auto-config classes | `setup_logging` (explicit) |
|
|
72
|
+
|
|
73
|
+
Out of scope for v1 (present in Java): JDBC/JPA, MongoDB, and Camunda logging.
|
|
74
|
+
|
|
75
|
+
## Context API
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from mobius_logging import context, LogType
|
|
79
|
+
|
|
80
|
+
context.init() # sets schema_version
|
|
81
|
+
context.tenant_id("tenant-123")
|
|
82
|
+
context.log_type(LogType.AUDIT)
|
|
83
|
+
context.resource_type("order")
|
|
84
|
+
context.resource_id("order-789")
|
|
85
|
+
context.field("custom_key", "value")
|
|
86
|
+
context.ensure_correlation_id() # generates one if missing
|
|
87
|
+
context.sync_trace_from_opentelemetry()
|
|
88
|
+
|
|
89
|
+
snap = context.snapshot()
|
|
90
|
+
try:
|
|
91
|
+
context.put("event_type", "order.approved")
|
|
92
|
+
finally:
|
|
93
|
+
context.restore(snap)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> Note: Java's `ensureCorrelationId()` has an inverted condition and only
|
|
97
|
+
> regenerates when one already exists. This port fixes that — it generates a
|
|
98
|
+
> correlation id when none is present.
|
|
99
|
+
|
|
100
|
+
## `@observed`
|
|
101
|
+
|
|
102
|
+
Works on sync and async functions. Adds `log_type`, `event_type`,
|
|
103
|
+
`resource_type`, `class_name`, `method_name`, `outcome`, `method_duration_ms`,
|
|
104
|
+
and `exception_type`/`exception_message` on failure. After completion it ships
|
|
105
|
+
the context to the schema producer (if one is registered), then restores the
|
|
106
|
+
pre-call context snapshot (Java clears MDC instead).
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
@observed(event_type="order.create", resource_type="order")
|
|
110
|
+
def create_order(order_id: str): ...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Kafka
|
|
114
|
+
|
|
115
|
+
### Schema event producer
|
|
116
|
+
|
|
117
|
+
The producer depends only on a small protocol, so the library has no hard Kafka
|
|
118
|
+
dependency. It matches the `py-kafka-producer-client` (>=0.1.7) signature — the
|
|
119
|
+
event is a dict and `topic` is keyword-only:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
class LogEventSender(Protocol):
|
|
123
|
+
def send(self, value: dict, *, topic: str) -> Any: ...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For the internal `py-kafka-producer-client`, pass the client to
|
|
127
|
+
`setup_logging(kafka_producer=...)` or wrap it directly:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from mobius_logging import KafkaLogProducer, PyKafkaProducerClientSender, set_log_producer
|
|
131
|
+
|
|
132
|
+
producer = KafkaLogProducer(PyKafkaProducerClientSender(client), "orders-service")
|
|
133
|
+
set_log_producer(producer)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`PyKafkaProducerClientSender` calls `client.send(value, topic=...)`, passing the
|
|
137
|
+
`DataIngestionOperation` envelope as a dict (the client serializes it). To use a
|
|
138
|
+
different client, pass any object exposing `send(value, *, topic)` as
|
|
139
|
+
`kafka_sender=`.
|
|
140
|
+
|
|
141
|
+
Sending runs on a background thread pool (best-effort, never raises, never
|
|
142
|
+
blocks the request) — the analog of the Java `@Async` method.
|
|
143
|
+
|
|
144
|
+
### Context propagation
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from mobius_logging import producer_headers, apply_consumer_record
|
|
148
|
+
|
|
149
|
+
# producer side
|
|
150
|
+
client.send(topic, value, headers=producer_headers())
|
|
151
|
+
|
|
152
|
+
# consumer side
|
|
153
|
+
apply_consumer_record(record.headers(), topic=record.topic(),
|
|
154
|
+
partition=record.partition(), offset=record.offset())
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Propagated header fields: `trace_id`, `span_id`, `correlation_id`,
|
|
158
|
+
`causation_id`, `tenant_id`. Consumer also sets `kafka_topic`,
|
|
159
|
+
`kafka_partition`, `kafka_offset`.
|
|
160
|
+
|
|
161
|
+
## Logging output
|
|
162
|
+
|
|
163
|
+
`configure_platform_logging(service_name, profile)` installs a root handler that
|
|
164
|
+
injects the current context onto every record. Profiles `staging`/`prod`/
|
|
165
|
+
`production` emit JSON; others use a readable text pattern. A daily-rotating
|
|
166
|
+
file handler writes to `logs/<service>.log` (30 days retained).
|
|
167
|
+
|
|
168
|
+
In JSON mode every context field is included automatically. For the text
|
|
169
|
+
pattern, `txn_id`, `tenant_id`, and `trace_id` are surfaced inline.
|
|
170
|
+
|
|
171
|
+
## Sensitive data masking
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from mobius_logging import mask, mask_map, init_custom_keys
|
|
175
|
+
|
|
176
|
+
init_custom_keys(["session_id", "refresh_token"])
|
|
177
|
+
mask("authorization", token) # masked
|
|
178
|
+
mask_map(context.copy()) # masks all sensitive keys in a dict
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Default sensitive keys: `password`, `token`, `secret`, `authorization`,
|
|
182
|
+
`cookie`, `apikey`, `privatekey`, `kubeconfig`. Matching is case-insensitive.
|
|
183
|
+
Masking keeps the last 4 characters of generic values and partially masks
|
|
184
|
+
emails — identical behaviour to the Java masker.
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python -m venv .venv && source .venv/bin/activate
|
|
190
|
+
pip install -e ".[dev]"
|
|
191
|
+
pytest
|
|
192
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mobius-logging-py"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Standardized logging context, observation, masking and Kafka schema events for Mobius Python (FastAPI) services."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Mobius Platform" }]
|
|
12
|
+
keywords = ["logging", "observability", "fastapi", "mobius", "kafka"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
# Schema-event producer used by @observed. Resolved from the internal index.
|
|
15
|
+
"py-kafka-producer-client>=0.1.7",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
# OpenTelemetry trace-id sync (sync_trace_from_opentelemetry is a no-op without it)
|
|
20
|
+
otel = ["opentelemetry-api>=1.20"]
|
|
21
|
+
# Web middleware target
|
|
22
|
+
fastapi = ["starlette>=0.27"]
|
|
23
|
+
# Dev / test tooling
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.4",
|
|
26
|
+
"starlette>=0.27",
|
|
27
|
+
"httpx>=0.24",
|
|
28
|
+
"opentelemetry-api>=1.20",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
mobius_logging = ["py.typed"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Mobius Logging for Python.
|
|
2
|
+
|
|
3
|
+
Python port of the Java Mobius logging library: standardized logging context,
|
|
4
|
+
method observation, sensitive-data masking, OpenTelemetry trace sync, JSON
|
|
5
|
+
logging and a Kafka schema-event producer for FastAPI / Starlette services.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from . import context, fields
|
|
10
|
+
from .context import (
|
|
11
|
+
clear,
|
|
12
|
+
correlation_id,
|
|
13
|
+
ensure_correlation_id,
|
|
14
|
+
field,
|
|
15
|
+
get,
|
|
16
|
+
init,
|
|
17
|
+
log_type,
|
|
18
|
+
put,
|
|
19
|
+
requester_type,
|
|
20
|
+
resource_id,
|
|
21
|
+
resource_type,
|
|
22
|
+
restore,
|
|
23
|
+
snapshot,
|
|
24
|
+
sync_trace_from_opentelemetry,
|
|
25
|
+
tenant_id,
|
|
26
|
+
transaction_id,
|
|
27
|
+
)
|
|
28
|
+
from .kafka_context import apply_consumer_record, producer_headers
|
|
29
|
+
from .kafka_producer import (
|
|
30
|
+
KafkaLogProducer,
|
|
31
|
+
LogEventSender,
|
|
32
|
+
PyKafkaProducerClientSender,
|
|
33
|
+
get_log_producer,
|
|
34
|
+
set_log_producer,
|
|
35
|
+
)
|
|
36
|
+
from .log_type import LogType
|
|
37
|
+
from .logging_config import configure_platform_logging
|
|
38
|
+
from .masking import init_custom_keys, mask, mask_map
|
|
39
|
+
from .middleware import PlatformLoggingMiddleware
|
|
40
|
+
from .observed import observed
|
|
41
|
+
from .bootstrap import setup_logging
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"context",
|
|
47
|
+
"fields",
|
|
48
|
+
"LogType",
|
|
49
|
+
"PlatformLoggingMiddleware",
|
|
50
|
+
"observed",
|
|
51
|
+
"setup_logging",
|
|
52
|
+
"configure_platform_logging",
|
|
53
|
+
"KafkaLogProducer",
|
|
54
|
+
"LogEventSender",
|
|
55
|
+
"PyKafkaProducerClientSender",
|
|
56
|
+
"set_log_producer",
|
|
57
|
+
"get_log_producer",
|
|
58
|
+
"producer_headers",
|
|
59
|
+
"apply_consumer_record",
|
|
60
|
+
"init_custom_keys",
|
|
61
|
+
"mask",
|
|
62
|
+
"mask_map",
|
|
63
|
+
# context functions
|
|
64
|
+
"put",
|
|
65
|
+
"get",
|
|
66
|
+
"field",
|
|
67
|
+
"init",
|
|
68
|
+
"clear",
|
|
69
|
+
"snapshot",
|
|
70
|
+
"restore",
|
|
71
|
+
"log_type",
|
|
72
|
+
"tenant_id",
|
|
73
|
+
"resource_id",
|
|
74
|
+
"resource_type",
|
|
75
|
+
"correlation_id",
|
|
76
|
+
"transaction_id",
|
|
77
|
+
"requester_type",
|
|
78
|
+
"ensure_correlation_id",
|
|
79
|
+
"sync_trace_from_opentelemetry",
|
|
80
|
+
]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""One-call bootstrap for FastAPI services.
|
|
2
|
+
|
|
3
|
+
Wires logging configuration, sensitive-key registration, the schema producer
|
|
4
|
+
and the request middleware in a single call - the rough equivalent of the
|
|
5
|
+
Java auto-configuration classes (which Spring applies automatically; Python
|
|
6
|
+
has no auto-config, so this is explicit).
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from kafka_producer_client import KafkaProducerConfig
|
|
11
|
+
from mobius_logging import setup_logging
|
|
12
|
+
|
|
13
|
+
setup_logging(
|
|
14
|
+
app,
|
|
15
|
+
service_name="orders-service",
|
|
16
|
+
profile="prod",
|
|
17
|
+
sensitive_keys=["session_id", "refresh_token"],
|
|
18
|
+
kafka_config=KafkaProducerConfig(
|
|
19
|
+
bootstrap_servers="broker:9092",
|
|
20
|
+
default_topic="construct-data-1",
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Any, Iterable, Optional
|
|
28
|
+
|
|
29
|
+
from . import masking
|
|
30
|
+
from .kafka_producer import (
|
|
31
|
+
KafkaLogProducer,
|
|
32
|
+
LogEventSender,
|
|
33
|
+
PyKafkaProducerClientSender,
|
|
34
|
+
set_log_producer,
|
|
35
|
+
)
|
|
36
|
+
from .logging_config import configure_platform_logging
|
|
37
|
+
from .middleware import PlatformLoggingMiddleware
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_logging(
|
|
43
|
+
app: Optional[Any] = None,
|
|
44
|
+
*,
|
|
45
|
+
service_name: str = "unknown-service",
|
|
46
|
+
profile: Optional[str] = None,
|
|
47
|
+
sensitive_keys: Optional[Iterable[str]] = None,
|
|
48
|
+
kafka_config: Optional[Any] = None,
|
|
49
|
+
kafka_producer: Optional[Any] = None,
|
|
50
|
+
kafka_sender: Optional[LogEventSender] = None,
|
|
51
|
+
enable_file: bool = True,
|
|
52
|
+
) -> Optional[KafkaLogProducer]:
|
|
53
|
+
"""Configure the library and (optionally) register FastAPI middleware.
|
|
54
|
+
|
|
55
|
+
The schema producer is resolved in this order:
|
|
56
|
+
|
|
57
|
+
1. ``kafka_sender`` - any object with ``send(value, *, topic)``;
|
|
58
|
+
2. ``kafka_producer`` - a ``py-kafka-producer-client`` ``KafkaProducerClient``
|
|
59
|
+
instance, wrapped automatically;
|
|
60
|
+
3. ``kafka_config`` - a ``KafkaProducerConfig``; the client's singleton is
|
|
61
|
+
configured and used (the default integration);
|
|
62
|
+
4. the already-configured ``py-kafka-producer-client`` singleton, if the app
|
|
63
|
+
called ``configure_kafka_producer(...)`` itself.
|
|
64
|
+
|
|
65
|
+
If none resolve, ``@observed`` simply skips schema publishing.
|
|
66
|
+
|
|
67
|
+
Returns the registered :class:`KafkaLogProducer`, if any.
|
|
68
|
+
"""
|
|
69
|
+
configure_platform_logging(service_name, profile, enable_file=enable_file)
|
|
70
|
+
masking.init_custom_keys(sensitive_keys)
|
|
71
|
+
|
|
72
|
+
sender = _resolve_sender(kafka_sender, kafka_producer, kafka_config)
|
|
73
|
+
|
|
74
|
+
producer: Optional[KafkaLogProducer] = None
|
|
75
|
+
if sender is not None:
|
|
76
|
+
producer = KafkaLogProducer(sender, service_name)
|
|
77
|
+
set_log_producer(producer)
|
|
78
|
+
else:
|
|
79
|
+
log.warning(
|
|
80
|
+
"mobius-logging: no Kafka producer resolved; @observed will not "
|
|
81
|
+
"publish schema events. Pass kafka_config/kafka_producer/kafka_sender."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if app is not None:
|
|
85
|
+
app.add_middleware(PlatformLoggingMiddleware, service_name=service_name)
|
|
86
|
+
|
|
87
|
+
return producer
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_sender(
|
|
91
|
+
kafka_sender: Optional[LogEventSender],
|
|
92
|
+
kafka_producer: Optional[Any],
|
|
93
|
+
kafka_config: Optional[Any],
|
|
94
|
+
) -> Optional[LogEventSender]:
|
|
95
|
+
if kafka_sender is not None:
|
|
96
|
+
return kafka_sender
|
|
97
|
+
if kafka_producer is not None:
|
|
98
|
+
return PyKafkaProducerClientSender(kafka_producer)
|
|
99
|
+
|
|
100
|
+
# Default: integrate with the internal py-kafka-producer-client singleton.
|
|
101
|
+
try:
|
|
102
|
+
from kafka_producer_client.action_logger import (
|
|
103
|
+
configure_kafka_producer,
|
|
104
|
+
get_kafka_producer,
|
|
105
|
+
)
|
|
106
|
+
except ImportError:
|
|
107
|
+
log.warning("mobius-logging: py-kafka-producer-client not installed.")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if kafka_config is not None:
|
|
112
|
+
configure_kafka_producer(kafka_config)
|
|
113
|
+
return PyKafkaProducerClientSender(get_kafka_producer())
|
|
114
|
+
except Exception: # noqa: BLE001 - producer is optional, never crash setup
|
|
115
|
+
log.warning(
|
|
116
|
+
"mobius-logging: could not initialize py-kafka-producer-client "
|
|
117
|
+
"(was configure_kafka_producer/kafka_config provided?).",
|
|
118
|
+
exc_info=True,
|
|
119
|
+
)
|
|
120
|
+
return None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Platform constants. Mirrors the Java ``PlatformConstant``.
|
|
2
|
+
|
|
3
|
+
The Kafka topic, schema id and tenant id are kept identical to the Java
|
|
4
|
+
library on purpose: Python services emit into the same schema pipeline.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
SCHEMA_VERSION = "mobius.log.v1"
|
|
11
|
+
|
|
12
|
+
# Masking
|
|
13
|
+
EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$")
|
|
14
|
+
# Mask every word char that is followed by at least 4 more word chars,
|
|
15
|
+
# i.e. keep the last 4 characters visible. Matches the Java MASKING_REGEX.
|
|
16
|
+
MASKING_RE = re.compile(r"\w(?=\w{4})")
|
|
17
|
+
MASKING_PATTERN = "*****"
|
|
18
|
+
AT_SYMBOL = "@"
|
|
19
|
+
STAR = "*"
|
|
20
|
+
|
|
21
|
+
# Kafka schema pipeline (must match the Java library)
|
|
22
|
+
TOPIC = "construct-data-1"
|
|
23
|
+
TENANT_ID = "2cf76e5f-26ad-4f2c-bccc-f4bc1e7bfb64"
|
|
24
|
+
SCHEMA_ID = "6a2f9b55f7827435c6ff87c4"
|
|
25
|
+
|
|
26
|
+
# Legacy -> canonical key remapping applied before shipping to the schema.
|
|
27
|
+
SANITIZE_KEYS = {
|
|
28
|
+
"agentId": "agent_id",
|
|
29
|
+
"userId": "user_id",
|
|
30
|
+
"tenantId": "tenant_id",
|
|
31
|
+
"txnId": "txn_id",
|
|
32
|
+
}
|