streamgate 3.0.0__tar.gz → 3.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.
- {streamgate-3.0.0 → streamgate-3.1.0}/CHANGELOG.md +43 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/CONFIGURATION.md +21 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/PKG-INFO +15 -6
- {streamgate-3.0.0 → streamgate-3.1.0}/README.md +14 -5
- {streamgate-3.0.0 → streamgate-3.1.0}/README.zh-CN.md +13 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/http_probe/consume.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/http_probe/produce.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/prod_pipeline/consume.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/prod_pipeline/produce.py +10 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/pure_pipeline/consume.py +4 -3
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/pure_pipeline/produce.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/redis_admission/produce.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/sqlite_upsert/consume.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/sqlite_upsert/produce.py +4 -4
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/__init__.py +3 -1
- streamgate-3.1.0/src/streamgate/obs/__init__.py +9 -0
- streamgate-3.1.0/src/streamgate/obs/formatter.py +57 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/obs/logging.py +4 -2
- streamgate-3.0.0/src/streamgate/transport/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/.github/workflows/ci.yml +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/.github/workflows/release.yml +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/.gitignore +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/AGENTS.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/CONTRIBUTING.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/LICENSE +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/docker-compose.yml +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/http_probe/README.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/http_probe/health_server.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/http_probe/models.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/prod_pipeline/README.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/prod_pipeline/health_server.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/prod_pipeline/models.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/pure_pipeline/README.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/pure_pipeline/models.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/redis_admission/README.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/redis_admission/models.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/sqlite_upsert/README.md +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/examples/sqlite_upsert/models.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/pyproject.toml +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/config.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/classifier.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/dlq.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/loop.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/options.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/consumer/runner.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/_deps.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/probe_signal.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/mssql_upsert/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/cache.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/carrier.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/config.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_cache.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_carrier.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/mssql.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/sqlite.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/backfill.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/classifier.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/config.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/engines.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/factory.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/upsert.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sqlite_upsert/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/ingest/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/in_memory.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/ingest/producer.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/obs/metrics.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/protocols.py +0 -0
- {streamgate-3.0.0/src/streamgate/obs → streamgate-3.1.0/src/streamgate/resilience}/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/resilience/backpressure.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/resilience/health.py +0 -0
- {streamgate-3.0.0/src/streamgate/resilience → streamgate-3.1.0/src/streamgate/transport}/__init__.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/transport/codec.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/transport/kafka.py +0 -0
- {streamgate-3.0.0 → streamgate-3.1.0}/uv.lock +0 -0
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.1.0] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`JsonFormatter`** — a built-in, out-of-the-box stdlib `logging.Formatter`
|
|
13
|
+
that renders every record as one JSON line, restoring the structured-field
|
|
14
|
+
visibility 2.x hosts got for free from loguru's auto-configured JSON sink.
|
|
15
|
+
3.0.0 kept emitting all structured fields (as flat `LogRecord` extras) but
|
|
16
|
+
stdlib's default formatter never renders them — a batch consumer logged just
|
|
17
|
+
the bare `batch_handle_success` event name, hiding `batch_size` /
|
|
18
|
+
`duration_ms`. Non-breaking; import from `streamgate` or `streamgate.obs`
|
|
19
|
+
and wire it in three lines:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import logging
|
|
23
|
+
from streamgate import JsonFormatter
|
|
24
|
+
|
|
25
|
+
handler = logging.StreamHandler() # stderr, same destination as 2.x
|
|
26
|
+
handler.setFormatter(JsonFormatter())
|
|
27
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Output layout per record: `timestamp` (UTC ISO-8601 with `Z` suffix, taken
|
|
31
|
+
from the `LogRecord` clock, not format time), `level`, `logger` (hierarchical
|
|
32
|
+
namespace — new versus 2.x), `event` (the stable event-name contract; host
|
|
33
|
+
`%s`-style messages render identically), then every flat extra field
|
|
34
|
+
(`batch_size`, `duration_ms`, `partition`, `offset`, ...), plus `error`
|
|
35
|
+
(formatted traceback) when a record carries `exc_info`. Serialization: one
|
|
36
|
+
`json.dumps` per record, `ensure_ascii=False`, `default=str` fallback.
|
|
37
|
+
The formatter is a generic component — any logger's records render correctly
|
|
38
|
+
through it (no extras ⇒ base fields only), and it never raises on
|
|
39
|
+
`LogRecord` reserved-attribute collisions (host-side extras are skipped;
|
|
40
|
+
rendering-side tolerance, the emission-side `emit()` fail-fast is unchanged).
|
|
41
|
+
Attach the handler to the root logger for unified host + streamgate JSON
|
|
42
|
+
logs, or to `logging.getLogger("streamgate")` for streamgate-only.
|
|
43
|
+
- `streamgate.obs.logging.RESERVED_RECORD_ATTRS` (also re-exported from
|
|
44
|
+
`streamgate.obs`) — the reserved-attribute set as a single shared constant,
|
|
45
|
+
used by `emit()` for fail-fast and by `JsonFormatter` for skip-tolerance
|
|
46
|
+
(previously privately defined per side).
|
|
47
|
+
- All `examples/` now configure logging through the built-in `JsonFormatter`,
|
|
48
|
+
so copy-pasting an example yields field-visible JSON logs out of the box
|
|
49
|
+
(previously `basicConfig(format=...)` rendered no extras).
|
|
50
|
+
|
|
8
51
|
## [3.0.0] - 2026-09-14
|
|
9
52
|
|
|
10
53
|
### BREAKING — loguru removed, logging switched to stdlib `logging`
|
|
@@ -29,6 +29,27 @@ streamgate emits logs through the standard-library `logging` module and ships
|
|
|
29
29
|
`streamgate.*` logger tree; level, handlers and formats are configured by the
|
|
30
30
|
host application (e.g. `logging.getLogger("streamgate")`).
|
|
31
31
|
|
|
32
|
+
For structured output, attach the built-in `JsonFormatter`
|
|
33
|
+
(`from streamgate import JsonFormatter`, also exported from
|
|
34
|
+
`streamgate.obs`): one JSON line per record with `timestamp` (UTC
|
|
35
|
+
ISO-8601, `Z` suffix) / `level` / `logger` / `event` base fields, every flat
|
|
36
|
+
extra field, and `error` when the record carries `exc_info`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import logging
|
|
40
|
+
from streamgate import JsonFormatter
|
|
41
|
+
|
|
42
|
+
handler = logging.StreamHandler() # stderr
|
|
43
|
+
handler.setFormatter(JsonFormatter())
|
|
44
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Rendering-side notes: the formatter is generic (any logger's records render
|
|
48
|
+
correctly — records without extras produce just the base fields) and never
|
|
49
|
+
raises on `LogRecord` reserved-attribute collisions (host-side extras are
|
|
50
|
+
skipped; only the library's own `emit()` fails fast on them). The reserved
|
|
51
|
+
attribute set is shared as `streamgate.obs.logging.RESERVED_RECORD_ATTRS`.
|
|
52
|
+
|
|
32
53
|
---
|
|
33
54
|
|
|
34
55
|
## Consumer (flat arguments)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: streamgate
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: A pure-core Kafka data pipeline framework: conditional admission, reliable delivery, and one obvious consume-loop outlet via a typed handler.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pwg-code/streamgate
|
|
6
6
|
Project-URL: Repository, https://github.com/pwg-code/streamgate
|
|
@@ -67,7 +67,7 @@ aiokafka gives you **transport**; streamgate gives you the **operating semantics
|
|
|
67
67
|
|
|
68
68
|
A complete produce → Kafka → consume → SQLite round trip that runs on a **bare install** (stdlib outlet, zero extra packages) — [`examples/pure_pipeline/`](examples/pure_pipeline/):
|
|
69
69
|
|
|
70
|
-
> Logging note: streamgate emits via stdlib `logging` and never configures it. Without host configuration only WARNING+ shows (stdlib `lastResort`);
|
|
70
|
+
> Logging note: streamgate emits via stdlib `logging` and never configures it. Without host configuration only WARNING+ shows (stdlib `lastResort`); wire the built-in [`JsonFormatter`](#logging) at your entry point (3 lines, above) to see INFO logs with every structured field rendered as JSON (the examples all do this — see [Logging](#logging)).
|
|
71
71
|
|
|
72
72
|
```python
|
|
73
73
|
import asyncio
|
|
@@ -233,16 +233,25 @@ There are exactly two sources of truth. **Required settings are true required co
|
|
|
233
233
|
|
|
234
234
|
## Logging
|
|
235
235
|
|
|
236
|
-
streamgate logs through the standard-library `logging` module — **the library emits records and never configures logging** (no handlers added, no levels changed at import). Every logger lives under the `streamgate` namespace (`streamgate.ingest.producer`, `streamgate.consumer.loop`, ...), so the whole tree is controlled by the host
|
|
236
|
+
streamgate logs through the standard-library `logging` module — **the library emits records and never configures logging** (no handlers added, no levels changed at import). Every logger lives under the `streamgate` namespace (`streamgate.ingest.producer`, `streamgate.consumer.loop`, ...), so the whole tree is controlled by the host. Event names (`batch_handle_start`, `poison_message_skipped`, ...) are a stable public contract; structured fields travel as flat `LogRecord` extras.
|
|
237
|
+
|
|
238
|
+
### JSON output: built-in `JsonFormatter`
|
|
239
|
+
|
|
240
|
+
stdlib's default formatter does **not** render `LogRecord` extras — with `basicConfig(format="%(message)s")` a batch consumer logs just `batch_handle_success`, hiding `batch_size` / `duration_ms`. Use the built-in `JsonFormatter` to make every structured field visible (this is what all examples do):
|
|
237
241
|
|
|
238
242
|
```python
|
|
239
243
|
import logging
|
|
240
244
|
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
from streamgate import JsonFormatter # equivalently: from streamgate.obs import JsonFormatter
|
|
246
|
+
|
|
247
|
+
handler = logging.StreamHandler() # stderr — same destination as 2.x
|
|
248
|
+
handler.setFormatter(JsonFormatter())
|
|
249
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
243
250
|
```
|
|
244
251
|
|
|
245
|
-
|
|
252
|
+
Each record renders as one JSON line: `timestamp` (UTC ISO-8601 with `Z` suffix, taken from the `LogRecord` clock), `level`, `logger` (the hierarchical namespace — a 3.0+ extra versus 2.x), `event` (the stable event name; host `%s`-style messages render the same way), then every flat extra field (`batch_size`, `duration_ms`, `partition`, `offset`, ...), plus `error` (formatted traceback) when a record carries `exc_info`. The formatter is a generic component — any logger renders correctly through it: records without extras produce just the base fields, and reserved-attribute collisions are skipped, never raised.
|
|
253
|
+
|
|
254
|
+
Attach the handler to the root logger as above and host + streamgate logs share one JSON format; attach the same way to `logging.getLogger("streamgate")` instead and only the streamgate tree is formatted. Level control is orthogonal: `logging.getLogger("streamgate").setLevel(logging.WARNING)` silences INFO noise regardless of formatter. Unconfigured hosts still see WARNING+ only (stdlib `lastResort` on stderr).
|
|
246
255
|
|
|
247
256
|
## Examples
|
|
248
257
|
|
|
@@ -32,7 +32,7 @@ aiokafka gives you **transport**; streamgate gives you the **operating semantics
|
|
|
32
32
|
|
|
33
33
|
A complete produce → Kafka → consume → SQLite round trip that runs on a **bare install** (stdlib outlet, zero extra packages) — [`examples/pure_pipeline/`](examples/pure_pipeline/):
|
|
34
34
|
|
|
35
|
-
> Logging note: streamgate emits via stdlib `logging` and never configures it. Without host configuration only WARNING+ shows (stdlib `lastResort`);
|
|
35
|
+
> Logging note: streamgate emits via stdlib `logging` and never configures it. Without host configuration only WARNING+ shows (stdlib `lastResort`); wire the built-in [`JsonFormatter`](#logging) at your entry point (3 lines, above) to see INFO logs with every structured field rendered as JSON (the examples all do this — see [Logging](#logging)).
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
import asyncio
|
|
@@ -198,16 +198,25 @@ There are exactly two sources of truth. **Required settings are true required co
|
|
|
198
198
|
|
|
199
199
|
## Logging
|
|
200
200
|
|
|
201
|
-
streamgate logs through the standard-library `logging` module — **the library emits records and never configures logging** (no handlers added, no levels changed at import). Every logger lives under the `streamgate` namespace (`streamgate.ingest.producer`, `streamgate.consumer.loop`, ...), so the whole tree is controlled by the host
|
|
201
|
+
streamgate logs through the standard-library `logging` module — **the library emits records and never configures logging** (no handlers added, no levels changed at import). Every logger lives under the `streamgate` namespace (`streamgate.ingest.producer`, `streamgate.consumer.loop`, ...), so the whole tree is controlled by the host. Event names (`batch_handle_start`, `poison_message_skipped`, ...) are a stable public contract; structured fields travel as flat `LogRecord` extras.
|
|
202
|
+
|
|
203
|
+
### JSON output: built-in `JsonFormatter`
|
|
204
|
+
|
|
205
|
+
stdlib's default formatter does **not** render `LogRecord` extras — with `basicConfig(format="%(message)s")` a batch consumer logs just `batch_handle_success`, hiding `batch_size` / `duration_ms`. Use the built-in `JsonFormatter` to make every structured field visible (this is what all examples do):
|
|
202
206
|
|
|
203
207
|
```python
|
|
204
208
|
import logging
|
|
205
209
|
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
from streamgate import JsonFormatter # equivalently: from streamgate.obs import JsonFormatter
|
|
211
|
+
|
|
212
|
+
handler = logging.StreamHandler() # stderr — same destination as 2.x
|
|
213
|
+
handler.setFormatter(JsonFormatter())
|
|
214
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
208
215
|
```
|
|
209
216
|
|
|
210
|
-
|
|
217
|
+
Each record renders as one JSON line: `timestamp` (UTC ISO-8601 with `Z` suffix, taken from the `LogRecord` clock), `level`, `logger` (the hierarchical namespace — a 3.0+ extra versus 2.x), `event` (the stable event name; host `%s`-style messages render the same way), then every flat extra field (`batch_size`, `duration_ms`, `partition`, `offset`, ...), plus `error` (formatted traceback) when a record carries `exc_info`. The formatter is a generic component — any logger renders correctly through it: records without extras produce just the base fields, and reserved-attribute collisions are skipped, never raised.
|
|
218
|
+
|
|
219
|
+
Attach the handler to the root logger as above and host + streamgate logs share one JSON format; attach the same way to `logging.getLogger("streamgate")` instead and only the streamgate tree is formatted. Level control is orthogonal: `logging.getLogger("streamgate").setLevel(logging.WARNING)` silences INFO noise regardless of formatter. Unconfigured hosts still see WARNING+ only (stdlib `lastResort` on stderr).
|
|
211
220
|
|
|
212
221
|
## Examples
|
|
213
222
|
|
|
@@ -372,16 +372,25 @@ pip install "streamgate[http]" # + HTTP 探活背压
|
|
|
372
372
|
|
|
373
373
|
## 日志
|
|
374
374
|
|
|
375
|
-
streamgate 走标准库 `logging`——**库只发日志记录、永不配置日志**(import 时不增删任何 handler、不改任何级别)。所有 logger 挂在 `streamgate` 命名空间下(`streamgate.ingest.producer`、`streamgate.consumer.loop`
|
|
375
|
+
streamgate 走标准库 `logging`——**库只发日志记录、永不配置日志**(import 时不增删任何 handler、不改任何级别)。所有 logger 挂在 `streamgate` 命名空间下(`streamgate.ingest.producer`、`streamgate.consumer.loop` 等),宿主整树控制。事件名(`batch_handle_start`、`poison_message_skipped` 等)是稳定的对外契约;结构化字段以 `LogRecord` extra 平铺。
|
|
376
|
+
|
|
377
|
+
### JSON 输出:内置 `JsonFormatter`
|
|
378
|
+
|
|
379
|
+
stdlib 默认 Formatter **不渲染** `LogRecord` extra——用 `basicConfig(format="%(message)s")` 时,批量消费只会打出裸事件名 `batch_handle_success`,看不到 `batch_size` / `duration_ms`。接内置 `JsonFormatter` 即可让全部结构化字段可见(所有示例均如此):
|
|
376
380
|
|
|
377
381
|
```python
|
|
378
382
|
import logging
|
|
379
383
|
|
|
380
|
-
|
|
381
|
-
|
|
384
|
+
from streamgate import JsonFormatter # 等价:from streamgate.obs import JsonFormatter
|
|
385
|
+
|
|
386
|
+
handler = logging.StreamHandler() # stderr,与 2.x 输出目标一致
|
|
387
|
+
handler.setFormatter(JsonFormatter())
|
|
388
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
382
389
|
```
|
|
383
390
|
|
|
384
|
-
|
|
391
|
+
每条日志渲染为一行 JSON:`timestamp`(UTC ISO8601,`Z` 后缀,取自 LogRecord 时钟)、`level`、`logger`(层级命名空间,2.x 没有的新字段)、`event`(稳定事件名;宿主 `%s` 风格日志同样适用),其余键为全部平铺的结构化字段(`batch_size`、`duration_ms`、`partition`、`offset` 等),记录携带 `exc_info` 时附 `error`(格式化堆栈)。Formatter 是通用组件——任何 logger 经它渲染都正确:无 extra 的记录只输出基础字段,与 LogRecord 保留属性撞键时直接跳过、不抛错。
|
|
392
|
+
|
|
393
|
+
按上面方式挂 root logger,宿主与 streamgate 日志统一为 JSON 格式;同样三行挂 `logging.getLogger("streamgate")` 则仅 streamgate 树生效。级别控制与 Formatter 正交:`logging.getLogger("streamgate").setLevel(logging.WARNING)` 可关掉 INFO 噪音。未配置日志的宿主仍只能看到 WARNING 及以上(stdlib `lastResort` 输出到 stderr)。
|
|
385
394
|
|
|
386
395
|
## 架构
|
|
387
396
|
|
|
@@ -13,7 +13,7 @@ import os
|
|
|
13
13
|
|
|
14
14
|
from health_server import serve_consumer_health
|
|
15
15
|
|
|
16
|
-
from streamgate import ConsumeContext, Consumer, JsonObject
|
|
16
|
+
from streamgate import ConsumeContext, Consumer, JsonFormatter, JsonObject
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
async def print_orders(
|
|
@@ -24,9 +24,9 @@ async def print_orders(
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
async def main() -> None:
|
|
27
|
-
logging.
|
|
28
|
-
|
|
29
|
-
)
|
|
27
|
+
handler = logging.StreamHandler()
|
|
28
|
+
handler.setFormatter(JsonFormatter())
|
|
29
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
30
30
|
consumer = Consumer(
|
|
31
31
|
bootstrap_servers=os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
32
32
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
|
|
16
16
|
from models import OrderIn
|
|
17
17
|
|
|
18
|
-
from streamgate import BackpressureConfig, Producer, ProducerOptions
|
|
18
|
+
from streamgate import BackpressureConfig, JsonFormatter, Producer, ProducerOptions
|
|
19
19
|
from streamgate.contrib.http_probe import HttpProbeSignal
|
|
20
20
|
|
|
21
21
|
|
|
@@ -38,9 +38,9 @@ def backpressure_config() -> BackpressureConfig:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
async def main() -> None:
|
|
41
|
-
logging.
|
|
42
|
-
|
|
43
|
-
)
|
|
41
|
+
handler = logging.StreamHandler()
|
|
42
|
+
handler.setFormatter(JsonFormatter())
|
|
43
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
44
44
|
producer = Producer(
|
|
45
45
|
os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
46
46
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -19,7 +19,7 @@ from models import Order
|
|
|
19
19
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
20
20
|
from sqlmodel import SQLModel
|
|
21
21
|
|
|
22
|
-
from streamgate import ConsumerOptions
|
|
22
|
+
from streamgate import ConsumerOptions, JsonFormatter
|
|
23
23
|
from streamgate.contrib.mssql_upsert import MssqlConsumer, Upsert
|
|
24
24
|
|
|
25
25
|
|
|
@@ -38,9 +38,9 @@ async def init_db() -> None:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
async def main() -> None:
|
|
41
|
-
logging.
|
|
42
|
-
|
|
43
|
-
)
|
|
41
|
+
handler = logging.StreamHandler()
|
|
42
|
+
handler.setFormatter(JsonFormatter())
|
|
43
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
44
44
|
await init_db()
|
|
45
45
|
consumer = MssqlConsumer(
|
|
46
46
|
db=db_conn(),
|
|
@@ -15,7 +15,13 @@ import os
|
|
|
15
15
|
|
|
16
16
|
from models import OrderIn
|
|
17
17
|
|
|
18
|
-
from streamgate import
|
|
18
|
+
from streamgate import (
|
|
19
|
+
BackpressureConfig,
|
|
20
|
+
DedupOptions,
|
|
21
|
+
JsonFormatter,
|
|
22
|
+
Producer,
|
|
23
|
+
ProducerOptions,
|
|
24
|
+
)
|
|
19
25
|
from streamgate.contrib.http_probe import HttpProbeSignal
|
|
20
26
|
from streamgate.contrib.redis_dedup import (
|
|
21
27
|
RedisConfig,
|
|
@@ -59,9 +65,9 @@ def backpressure_config() -> BackpressureConfig:
|
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
async def main() -> None:
|
|
62
|
-
logging.
|
|
63
|
-
|
|
64
|
-
)
|
|
68
|
+
handler = logging.StreamHandler()
|
|
69
|
+
handler.setFormatter(JsonFormatter())
|
|
70
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
65
71
|
producer = Producer(
|
|
66
72
|
os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:9092"),
|
|
67
73
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -18,6 +18,7 @@ from streamgate import (
|
|
|
18
18
|
Consumer,
|
|
19
19
|
ConsumerOptions,
|
|
20
20
|
DlqOptions,
|
|
21
|
+
JsonFormatter,
|
|
21
22
|
JsonObject,
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -58,9 +59,9 @@ async def handle_orders(
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
async def main() -> None:
|
|
61
|
-
logging.
|
|
62
|
-
|
|
63
|
-
)
|
|
62
|
+
handler = logging.StreamHandler()
|
|
63
|
+
handler.setFormatter(JsonFormatter())
|
|
64
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
64
65
|
consumer = Consumer(
|
|
65
66
|
bootstrap_servers=os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
66
67
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
|
|
15
15
|
from models import OrderIn
|
|
16
16
|
|
|
17
|
-
from streamgate import DedupOptions, Producer, ProducerOptions
|
|
17
|
+
from streamgate import DedupOptions, JsonFormatter, Producer, ProducerOptions
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def bootstrap_servers() -> str:
|
|
@@ -23,9 +23,9 @@ def bootstrap_servers() -> str:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
async def main() -> None:
|
|
26
|
-
logging.
|
|
27
|
-
|
|
28
|
-
)
|
|
26
|
+
handler = logging.StreamHandler()
|
|
27
|
+
handler.setFormatter(JsonFormatter())
|
|
28
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
29
29
|
producer = Producer(
|
|
30
30
|
bootstrap_servers(),
|
|
31
31
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
|
|
16
16
|
from models import OrderIn
|
|
17
17
|
|
|
18
|
-
from streamgate import DedupOptions, Producer, ProducerOptions
|
|
18
|
+
from streamgate import DedupOptions, JsonFormatter, Producer, ProducerOptions
|
|
19
19
|
from streamgate.contrib.redis_dedup import (
|
|
20
20
|
RedisConfig,
|
|
21
21
|
RedisDedupCache,
|
|
@@ -41,9 +41,9 @@ def build_carrier() -> RedisDedupCarrier[OrderIn]:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
async def main() -> None:
|
|
44
|
-
logging.
|
|
45
|
-
|
|
46
|
-
)
|
|
44
|
+
handler = logging.StreamHandler()
|
|
45
|
+
handler.setFormatter(JsonFormatter())
|
|
46
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
47
47
|
carrier = build_carrier()
|
|
48
48
|
producer = Producer(
|
|
49
49
|
os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
@@ -13,14 +13,14 @@ import os
|
|
|
13
13
|
|
|
14
14
|
from models import Order
|
|
15
15
|
|
|
16
|
-
from streamgate import ConsumerOptions
|
|
16
|
+
from streamgate import ConsumerOptions, JsonFormatter
|
|
17
17
|
from streamgate.contrib.sqlite_upsert import SqliteConsumer, Upsert
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
async def main() -> None:
|
|
21
|
-
logging.
|
|
22
|
-
|
|
23
|
-
)
|
|
21
|
+
handler = logging.StreamHandler()
|
|
22
|
+
handler.setFormatter(JsonFormatter())
|
|
23
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
24
24
|
consumer = SqliteConsumer(
|
|
25
25
|
db=os.environ.get("DB_CONN", "sqlite+aiosqlite:///./data/streamgate.db"),
|
|
26
26
|
upserts=[Upsert(model=Order, keys=["order_id"])],
|
|
@@ -10,13 +10,13 @@ import os
|
|
|
10
10
|
|
|
11
11
|
from models import OrderIn
|
|
12
12
|
|
|
13
|
-
from streamgate import Producer, ProducerOptions
|
|
13
|
+
from streamgate import JsonFormatter, Producer, ProducerOptions
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
async def main() -> None:
|
|
17
|
-
logging.
|
|
18
|
-
|
|
19
|
-
)
|
|
17
|
+
handler = logging.StreamHandler()
|
|
18
|
+
handler.setFormatter(JsonFormatter())
|
|
19
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
20
20
|
producer = Producer(
|
|
21
21
|
os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
22
22
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|
|
@@ -52,6 +52,7 @@ from streamgate.ingest.producer import (
|
|
|
52
52
|
Producer,
|
|
53
53
|
ProducerOptions,
|
|
54
54
|
)
|
|
55
|
+
from streamgate.obs import JsonFormatter
|
|
55
56
|
from streamgate.obs.metrics import LoggingMetricsSink, MetricsSink
|
|
56
57
|
from streamgate.protocols import (
|
|
57
58
|
AllowAllSignal,
|
|
@@ -85,7 +86,7 @@ from streamgate.resilience.health import (
|
|
|
85
86
|
from streamgate.transport.codec import JsonEnvelopeCodec
|
|
86
87
|
from streamgate.transport.kafka import KafkaConsumerService
|
|
87
88
|
|
|
88
|
-
__version__ = "3.
|
|
89
|
+
__version__ = "3.1.0"
|
|
89
90
|
|
|
90
91
|
__all__ = [
|
|
91
92
|
"AllowAllSignal",
|
|
@@ -115,6 +116,7 @@ __all__ = [
|
|
|
115
116
|
"InMemoryDedupCarrier",
|
|
116
117
|
"JsonObject",
|
|
117
118
|
"JsonEnvelopeCodec",
|
|
119
|
+
"JsonFormatter",
|
|
118
120
|
"KafkaConfig",
|
|
119
121
|
"KafkaConsumerService",
|
|
120
122
|
"LoggingMetricsSink",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""内置 JSON Formatter:宿主开箱即用的结构化日志渲染。
|
|
2
|
+
|
|
3
|
+
3.0.0 移除 loguru 后,``emit()`` 经 ``extra=fields`` 发出的结构化字段仍在
|
|
4
|
+
LogRecord 上,但 stdlib 默认 Formatter 不渲染 extra——字段"看不见"。
|
|
5
|
+
本模块补齐渲染侧:每条日志渲染为一行 JSON(基础字段 + extra 平铺 +
|
|
6
|
+
exc_info 兜底)。定位是通用组件:任何 logger(含宿主自身的)经它渲染
|
|
7
|
+
都正确;本模块同样不做任何 logging 全局配置(挂载方式归宿主)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
from streamgate.obs.logging import RESERVED_RECORD_ATTRS
|
|
16
|
+
|
|
17
|
+
_BASE_FIELDS: Final[frozenset[str]] = frozenset(
|
|
18
|
+
{"timestamp", "level", "logger", "event"}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _iso_utc(epoch_seconds: float) -> str:
|
|
23
|
+
"""LogRecord 时间戳 → UTC ISO8601(毫秒精度,Z 后缀)。
|
|
24
|
+
|
|
25
|
+
必须取自 LogRecord 时钟(record.created)而非 format 时刻,避免漂移。
|
|
26
|
+
"""
|
|
27
|
+
moment = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc)
|
|
28
|
+
return moment.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class JsonFormatter(logging.Formatter):
|
|
32
|
+
"""stdlib ``logging.Formatter`` 子类:每条日志渲染为一行 JSON。
|
|
33
|
+
|
|
34
|
+
字段布局:``timestamp``(UTC ISO8601,Z 后缀)/ ``level`` /
|
|
35
|
+
``logger``(层级命名空间)/ ``event``(``record.getMessage()``,
|
|
36
|
+
兼容宿主 ``%s`` 风格日志),其余键从 ``record.__dict__`` 剔除保留
|
|
37
|
+
属性后平铺(即 ``emit(extra=...)`` 注入的全部结构化字段);
|
|
38
|
+
``record.exc_info`` 存在时附 ``error``(formatException 输出)。
|
|
39
|
+
序列化:``ensure_ascii=False``(中文原样)、``default=str`` 兜底。
|
|
40
|
+
与发射侧 emit 的 fail-fast 不同层:遇保留属性撞键直接跳过、不抛错
|
|
41
|
+
(渲染侧必须容错——宿主自己的 extra 不受 emit 保护)。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
45
|
+
entry: dict[str, object] = {
|
|
46
|
+
"timestamp": _iso_utc(record.created),
|
|
47
|
+
"level": record.levelname,
|
|
48
|
+
"logger": record.name,
|
|
49
|
+
"event": record.getMessage(),
|
|
50
|
+
}
|
|
51
|
+
for key, value in record.__dict__.items():
|
|
52
|
+
if key in RESERVED_RECORD_ATTRS or key in _BASE_FIELDS:
|
|
53
|
+
continue
|
|
54
|
+
entry[key] = value
|
|
55
|
+
if record.exc_info is not None:
|
|
56
|
+
entry["error"] = self.formatException(record.exc_info)
|
|
57
|
+
return json.dumps(entry, ensure_ascii=False, default=str)
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
``emit`` 是内部发射辅助:保持调用点单行形态,并统一防御
|
|
7
7
|
(结构化字段名与 LogRecord 保留属性冲突时 fail-fast)。
|
|
8
8
|
本模块不触碰任何 logging 全局状态(无 handler 增删、无级别修改)。
|
|
9
|
+
``RESERVED_RECORD_ATTRS`` 是保留属性集合的唯一来源:发射侧据此 fail-fast,
|
|
10
|
+
渲染侧(formatter.JsonFormatter)据此做"跳过"容错,两处禁止各自定义。
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
import logging
|
|
@@ -20,14 +22,14 @@ _LEVELS: Final[dict[str, int]] = {
|
|
|
20
22
|
|
|
21
23
|
# LogRecord 实例属性 + 格式化期追加属性(asctime/message/exc_text):
|
|
22
24
|
# extra 携带这些键会在 stdlib 内部炸 KeyError,这里提前 fail-fast 并给出可读错误
|
|
23
|
-
|
|
25
|
+
RESERVED_RECORD_ATTRS: Final[frozenset[str]] = frozenset(
|
|
24
26
|
logging.LogRecord("", 0, "", 0, "", (), None).__dict__
|
|
25
27
|
) | {"asctime", "exc_text", "message", "taskName"}
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def emit(logger: logging.Logger, level: str, event: str, /, **fields: object) -> None:
|
|
29
31
|
"""发射一条结构化日志记录:event 为事件名(对外契约,勿改语义),fields 平铺进 extra。"""
|
|
30
|
-
conflicts = fields.keys() &
|
|
32
|
+
conflicts = fields.keys() & RESERVED_RECORD_ATTRS
|
|
31
33
|
if conflicts:
|
|
32
34
|
names = ", ".join(sorted(conflicts))
|
|
33
35
|
raise ValueError(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{streamgate-3.0.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{streamgate-3.0.0/src/streamgate/obs → streamgate-3.1.0/src/streamgate/resilience}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{streamgate-3.0.0/src/streamgate/resilience → streamgate-3.1.0/src/streamgate/transport}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|