streamgate 2.1.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-2.1.0 → streamgate-3.1.0}/.github/workflows/ci.yml +1 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/CHANGELOG.md +144 -0
- streamgate-3.1.0/CONFIGURATION.md +217 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/PKG-INFO +29 -6
- {streamgate-2.1.0 → streamgate-3.1.0}/README.md +28 -4
- {streamgate-2.1.0 → streamgate-3.1.0}/README.zh-CN.md +26 -4
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/consume.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/health_server.py +6 -3
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/produce.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/consume.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/health_server.py +6 -3
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/produce.py +11 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/consume.py +5 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/produce.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/produce.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/consume.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/produce.py +5 -1
- {streamgate-2.1.0 → streamgate-3.1.0}/pyproject.toml +2 -2
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/__init__.py +6 -6
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/config.py +8 -7
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/dlq.py +12 -12
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/loop.py +35 -32
- streamgate-3.1.0/src/streamgate/consumer/options.py +123 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/runner.py +32 -70
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/probe_signal.py +20 -16
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/mssql_upsert/__init__.py +11 -6
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/cache.py +8 -5
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/carrier.py +16 -13
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_cache.py +7 -4
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_carrier.py +19 -16
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/backfill.py +8 -5
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/engines.py +6 -2
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/factory.py +11 -5
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/upsert.py +12 -9
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sqlite_upsert/__init__.py +11 -6
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/producer.py +44 -82
- streamgate-3.1.0/src/streamgate/obs/__init__.py +9 -0
- streamgate-3.1.0/src/streamgate/obs/formatter.py +57 -0
- streamgate-3.1.0/src/streamgate/obs/logging.py +39 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/obs/metrics.py +3 -2
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/resilience/health.py +5 -2
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/transport/codec.py +6 -3
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/transport/kafka.py +16 -18
- {streamgate-2.1.0 → streamgate-3.1.0}/uv.lock +2 -33
- streamgate-2.1.0/CONFIGURATION.md +0 -190
- streamgate-2.1.0/src/streamgate/consumer/options.py +0 -261
- streamgate-2.1.0/src/streamgate/obs/logging.py +0 -43
- streamgate-2.1.0/src/streamgate/transport/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/.github/workflows/release.yml +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/.gitignore +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/AGENTS.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/CONTRIBUTING.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/LICENSE +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/docker-compose.yml +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/README.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/models.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/README.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/models.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/README.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/models.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/README.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/models.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/README.md +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/models.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/classifier.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/_deps.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/config.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/mssql.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/sqlite.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/classifier.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/config.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/in_memory.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/protocols.py +0 -0
- {streamgate-2.1.0/src/streamgate/obs → streamgate-3.1.0/src/streamgate/resilience}/__init__.py +0 -0
- {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/resilience/backpressure.py +0 -0
- {streamgate-2.1.0/src/streamgate/resilience → streamgate-3.1.0/src/streamgate/transport}/__init__.py +0 -0
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
bare-install:
|
|
50
50
|
# Regression gate for the pure-core contract: a bare install
|
|
51
|
-
# (`pip install .`) must pull exactly aiokafka/
|
|
51
|
+
# (`pip install .`) must pull exactly aiokafka/pydantic (+transitive),
|
|
52
52
|
# import cleanly, and run the pure_pipeline example end to end
|
|
53
53
|
# (ingest → Kafka → consume → sqlite sink) against a real broker.
|
|
54
54
|
runs-on: ubuntu-latest
|
|
@@ -5,6 +5,150 @@ 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
|
+
|
|
51
|
+
## [3.0.0] - 2026-09-14
|
|
52
|
+
|
|
53
|
+
### BREAKING — loguru removed, logging switched to stdlib `logging`
|
|
54
|
+
|
|
55
|
+
The library no longer depends on loguru. All internal loggers now live under
|
|
56
|
+
the standard-library `streamgate.*` logger tree
|
|
57
|
+
(`streamgate.ingest.producer`, `streamgate.consumer.loop`, ...), and the
|
|
58
|
+
library **only emits records — it never configures logging**: `import
|
|
59
|
+
streamgate` adds no handlers, changes no levels, touches no logging global
|
|
60
|
+
state. Wheel dependencies drop from three to two (`aiokafka`, `pydantic`).
|
|
61
|
+
|
|
62
|
+
**Log event names and structured field names are unchanged** — only the
|
|
63
|
+
transport moved. What breaks and how to migrate:
|
|
64
|
+
|
|
65
|
+
| 2.x behavior | 3.0.0 replacement |
|
|
66
|
+
|--------------|-------------------|
|
|
67
|
+
| `from streamgate import logger` | `logging.getLogger("streamgate")` (module-level loggers are `logging.getLogger(__name__)`) |
|
|
68
|
+
| `configure_logger(level=..., fmt=...)` | removed — configure stdlib logging in your app (`logging.basicConfig` / dictConfig) |
|
|
69
|
+
| auto-configured stderr JSON sink on `import streamgate` | gone — unconfigured hosts see WARNING+ only via stdlib `lastResort`; add `logging.basicConfig(level=logging.INFO, ...)` to restore visibility |
|
|
70
|
+
| JSON parsing of the library's stderr output (`{"timestamp", "level", "event", ...extra}`) | attach an equivalent stdlib JSON formatter yourself (recipe below) |
|
|
71
|
+
| per-level control of library logs | `logging.getLogger("streamgate").setLevel(logging.WARNING)` (or any subtree) |
|
|
72
|
+
|
|
73
|
+
Equivalent JSON formatter recipe (fields `timestamp` / `level` / `event` plus
|
|
74
|
+
flat extras, UTC ISO-8601):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import json
|
|
78
|
+
import logging
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class JsonFormatter(logging.Formatter):
|
|
82
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
83
|
+
entry = {
|
|
84
|
+
"timestamp": self.formatTime(record, datefmt="%Y-%m-%dT%H:%M:%S%z"),
|
|
85
|
+
"level": record.levelname,
|
|
86
|
+
"event": record.getMessage(),
|
|
87
|
+
}
|
|
88
|
+
reserved = set(logging.LogRecord("", 0, "", 0, "", (), None).__dict__) | {
|
|
89
|
+
"asctime", "message", "taskName",
|
|
90
|
+
}
|
|
91
|
+
for key, value in record.__dict__.items():
|
|
92
|
+
if key not in reserved and key not in entry:
|
|
93
|
+
entry[key] = value
|
|
94
|
+
return json.dumps(entry, ensure_ascii=False, default=str)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
handler = logging.StreamHandler()
|
|
98
|
+
handler.setFormatter(JsonFormatter())
|
|
99
|
+
logging.getLogger("streamgate").addHandler(handler)
|
|
100
|
+
logging.getLogger("streamgate").setLevel(logging.INFO)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
|
|
105
|
+
- `streamgate.obs.logging.emit` is the new internal emission helper; passing a
|
|
106
|
+
structured field whose name collides with a `LogRecord` reserved attribute
|
|
107
|
+
(`message`, `exc_info`, `asctime`, ...) raises `ValueError` (fail-fast) —
|
|
108
|
+
rename such fields. The one internal `exc_info=True` site now uses stdlib's
|
|
109
|
+
native `exc_info` parameter.
|
|
110
|
+
- `LoggingMetricsSink` keeps its level-string dispatch (`debug` / `info` /
|
|
111
|
+
`warning` / `error`, default `info`) and now writes through stdlib logging.
|
|
112
|
+
|
|
113
|
+
### BREAKING — environment-variable fallbacks removed
|
|
114
|
+
|
|
115
|
+
The library no longer reads environment variables. Configuration has exactly
|
|
116
|
+
two sources of truth: **required settings are true required constructor
|
|
117
|
+
arguments** — missing one raises a native Python `TypeError` (missing
|
|
118
|
+
argument), never a silently guessed default such as `kafka:9092` — and
|
|
119
|
+
**optional settings hold their built-in defaults directly** on the options
|
|
120
|
+
objects (`RuntimeTuning` fields are plain `int` / `float` / `str`, not
|
|
121
|
+
`int | None` placeholders). Every `KAFKA__*` / `CONSUMER__*` / `METRICS__*` /
|
|
122
|
+
`BACKPRESSURE__*` key is gone. Hosts that injected configuration via the
|
|
123
|
+
environment (K8s ConfigMap, docker-compose) must resolve it themselves and
|
|
124
|
+
pass the values explicitly after upgrading: a missing required argument now
|
|
125
|
+
fails loudly at construction instead of silently connecting to the wrong
|
|
126
|
+
cluster, and optional knobs fall back to the built-in defaults instead of the
|
|
127
|
+
env value. Migrate key by key:
|
|
128
|
+
|
|
129
|
+
| Old behavior (≤2.x and earlier in this release) | 3.0.0 |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `Producer(bootstrap_servers=None → KAFKA__BOOTSTRAP_SERVERS → "kafka:9092")` | `Producer(bootstrap_servers, topic, key=None, options=...)` — both truly required, no default |
|
|
132
|
+
| `Consumer(topic=None → KAFKA__TOPIC fail-fast ValueError)` | `Consumer(bootstrap_servers, topic, group_id, handler)` — all truly required; missing ⇒ native `TypeError` |
|
|
133
|
+
| `DlqOptions.enabled=None → CONSUMER__DLQ_ENABLED → True` | `enabled: bool = True` |
|
|
134
|
+
| `DlqOptions.send_retries=None → CONSUMER__DLQ_SEND_RETRIES → 3` | `send_retries: int = 3` |
|
|
135
|
+
| `DlqOptions.topic=None → KAFKA__DLQ_TOPIC` | `topic: str \| None = None` — still required when DLQ is enabled (runtime validation preserved) |
|
|
136
|
+
| `RuntimeTuning` 9 fields `None → CONSUMER__* → default` | fields hold defaults directly (`max_retries=3`, `retry_backoff_base=1.0`, `reconnect_base=1.0`, `reconnect_max=30.0`, `max_poll_records=500`, `session_timeout_ms=30000`, `max_poll_interval_ms=300000`, `auto_offset_reset="earliest"`, `backlog_check_interval=30.0`) |
|
|
137
|
+
| `METRICS__WINDOW_SECONDS` | `metrics_window_seconds=60` passed explicitly on `ProducerOptions` / `ConsumerOptions` |
|
|
138
|
+
| `CONSUMER__BATCH_SIZE` / `CONSUMER__FLUSH_TIMEOUT_SECONDS` | `batch_size` / `flush_timeout` explicit arguments (defaults unchanged: `500` / `5.0`) |
|
|
139
|
+
| `SqliteConsumer(...) / MssqlConsumer(...)` identity arguments optional | `bootstrap_servers` / `topic` / `group_id` are required keyword arguments, mirroring the core `Consumer` |
|
|
140
|
+
| `streamgate.consumer.options.resolve_tuning` / `ResolvedRuntimeTuning` | removed (internal resolution layer); `RuntimeTuning` holds concrete values and its own `backoff_seconds()` |
|
|
141
|
+
|
|
142
|
+
Notes:
|
|
143
|
+
|
|
144
|
+
- All runtime validations are preserved (not fallbacks): DLQ topic required
|
|
145
|
+
when DLQ is enabled, metrics window range 1–600, `batch_size >= 1`,
|
|
146
|
+
`flush_timeout > 0`.
|
|
147
|
+
- `KafkaConfig` / `BackpressureConfig` field defaults are unchanged — they
|
|
148
|
+
are pure config objects and never read the environment; the `Producer`
|
|
149
|
+
constructor values always override `KafkaConfig.bootstrap_servers` /
|
|
150
|
+
`.topic`.
|
|
151
|
+
|
|
8
152
|
## [2.1.0] - 2026-09-11
|
|
9
153
|
|
|
10
154
|
### Added
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
streamgate components are configured with typed parameters and Pydantic
|
|
4
|
+
objects. There are exactly **two sources of truth**:
|
|
5
|
+
|
|
6
|
+
1. **Required settings are true required constructor arguments** —
|
|
7
|
+
`Producer(bootstrap_servers, topic, ...)` and
|
|
8
|
+
`Consumer(bootstrap_servers, topic, group_id, handler)`. Missing one fails
|
|
9
|
+
with a native Python `TypeError` (missing argument) at construction. The
|
|
10
|
+
library never reads environment variables and never silently defaults a
|
|
11
|
+
required setting.
|
|
12
|
+
2. **Optional settings directly hold their built-in defaults** — advanced
|
|
13
|
+
knobs fold into `ProducerOptions` / `ConsumerOptions` (dedup, backpressure,
|
|
14
|
+
DLQ, tuning, metrics window); omit them and the documented defaults apply.
|
|
15
|
+
|
|
16
|
+
If your host wants env-driven configuration, resolve the environment yourself
|
|
17
|
+
and pass the values explicitly (the examples do exactly that with
|
|
18
|
+
`os.environ.get(..., default)`).
|
|
19
|
+
|
|
20
|
+
Database and Redis connection settings are **not part of the core framework**:
|
|
21
|
+
storage carriers are injected and their configuration belongs to your
|
|
22
|
+
application. The official strategy implementations under `streamgate.contrib`
|
|
23
|
+
ship typed config objects (below) that you construct and pass to the factories.
|
|
24
|
+
|
|
25
|
+
## Logging (not a configuration surface)
|
|
26
|
+
|
|
27
|
+
streamgate emits logs through the standard-library `logging` module and ships
|
|
28
|
+
**no logging configuration** — the library only emits records under the
|
|
29
|
+
`streamgate.*` logger tree; level, handlers and formats are configured by the
|
|
30
|
+
host application (e.g. `logging.getLogger("streamgate")`).
|
|
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
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Consumer (flat arguments)
|
|
56
|
+
|
|
57
|
+
| Argument | Default | Description |
|
|
58
|
+
|----------|---------|-------------|
|
|
59
|
+
| `bootstrap_servers` | **required** | Comma-separated broker list. Missing ⇒ native `TypeError`. |
|
|
60
|
+
| `topic` | **required** | Topic to consume from. Missing ⇒ native `TypeError`. |
|
|
61
|
+
| `group_id` | **required** | Kafka consumer group id. Missing ⇒ native `TypeError`. |
|
|
62
|
+
| `handler` | **required** | Async callable `handler(batch, context)` — the only outlet. |
|
|
63
|
+
| `batch_size` | `500` | Records buffered before a flush. `1` = single-record real-time. |
|
|
64
|
+
| `flush_timeout` | `5.0` | Max wait before a partially filled batch is flushed. |
|
|
65
|
+
| `options` | `None` | `ConsumerOptions` — everything below. |
|
|
66
|
+
|
|
67
|
+
## ConsumerOptions
|
|
68
|
+
|
|
69
|
+
| Field | Default | Description |
|
|
70
|
+
|-------|---------|-------------|
|
|
71
|
+
| `expected_type` | `None` | Envelope `type` validation (`None` = accept any; mismatched records are quarantined to the DLQ). |
|
|
72
|
+
| `probe` | `None` | Single-record callable with handler-equivalent, idempotent semantics. Provided ⇒ POISON batches are located record-by-record and bad records quarantined precisely; omitted ⇒ the whole batch is quarantined. |
|
|
73
|
+
| `classifier` | `DefaultErrorClassifier` | RETRY/POISON/FATAL mapping for handler exceptions. Connecting a DB? Inject a DB-aware classifier (`contrib.sql_upsert.SQLAlchemyErrorClassifier`). |
|
|
74
|
+
| `persist_hook` | `None` | `DedupCarrier` hooked into the consume loop: `on_persisted` runs after each successful batch (dedup linkage with the producer side). Lifecycle is framework-managed. |
|
|
75
|
+
| `collapse_key` | `None` | In-batch dedup key (keeps the last record) applied to hook notifications. |
|
|
76
|
+
| `log_context` | `None` | Per-record extra fields for quarantine logs. |
|
|
77
|
+
| `codec` | `JsonEnvelopeCodec` | Message envelope codec. |
|
|
78
|
+
| `health_probe` | `None` | Side-channel health component; implements `check_health()` ⇒ observed in snapshots; `start()`/`close()` ⇒ lifecycle managed. |
|
|
79
|
+
| `backlog_ttl_seconds` | `18000` | Backlog alert budget: WARN > TTL/2, ERROR > TTL×0.8. |
|
|
80
|
+
| `metrics_window_seconds` | `60` | Sliding-window length (1–600) for rate/latency snapshot fields; out-of-range values fail at construction. |
|
|
81
|
+
| `dlq` | `DlqOptions()` | DLQ options (below). |
|
|
82
|
+
| `tuning` | `RuntimeTuning()` | Runtime tuning (below). |
|
|
83
|
+
|
|
84
|
+
## DlqOptions
|
|
85
|
+
|
|
86
|
+
| Field | Default | Description |
|
|
87
|
+
|-------|---------|-------------|
|
|
88
|
+
| `enabled` | `true` | Master switch; `false` falls back to the paused behavior (emergency escape hatch). |
|
|
89
|
+
| `topic` | `None` | Dead-letter topic; required when DLQ is enabled (validated at DLQ-producer construction). |
|
|
90
|
+
| `message_type` | `streamgate_dlq` | Envelope `type` of DLQ messages. |
|
|
91
|
+
| `send_retries` | `3` | Total attempts per DLQ send (exhausting them pauses the batch). |
|
|
92
|
+
|
|
93
|
+
## RuntimeTuning
|
|
94
|
+
|
|
95
|
+
Every field directly holds its built-in default:
|
|
96
|
+
|
|
97
|
+
| Field | Default | Description |
|
|
98
|
+
|-------|---------|-------------|
|
|
99
|
+
| `max_retries` | `3` | Handler retries per batch. |
|
|
100
|
+
| `retry_backoff_base` | `1.0` | Retry backoff base (seconds, exponential). |
|
|
101
|
+
| `reconnect_base` | `1.0` | Reconnect/paused-recovery backoff base (seconds). |
|
|
102
|
+
| `reconnect_max` | `30.0` | Reconnect backoff cap (seconds). |
|
|
103
|
+
| `max_poll_records` | `500` | Max records per poll. |
|
|
104
|
+
| `session_timeout_ms` | `30000` | Kafka session timeout. |
|
|
105
|
+
| `max_poll_interval_ms` | `300000` | Max time between polls before rebalance. |
|
|
106
|
+
| `auto_offset_reset` | `earliest` | Offset reset policy. |
|
|
107
|
+
| `backlog_check_interval` | `30.0` | Backlog age check period (seconds). |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Contrib strategy configs
|
|
112
|
+
|
|
113
|
+
These are plain constructor arguments. All belong to `streamgate.contrib`
|
|
114
|
+
packages — install the matching extra first (`[redis]` / `[sql]` / `[http]`).
|
|
115
|
+
|
|
116
|
+
### `contrib.redis_dedup.RedisConfig`
|
|
117
|
+
|
|
118
|
+
| Field | Default | Description |
|
|
119
|
+
|-------|---------|-------------|
|
|
120
|
+
| `url` | `redis://localhost:6379/0` | Redis connection URL. |
|
|
121
|
+
| `key_prefix` | `streamgate:` | Key prefix for dedup keys (`<prefix>dedup:<identity>`). |
|
|
122
|
+
| `identity_ttl_seconds` | `18000` | Single-key placeholder/summary TTL (idle-GC heartbeat: every write renews it). |
|
|
123
|
+
| `group_key_prefix` | `dedup_group:` | Group-carrier key prefix (`<prefix>dedup_group:<group>` HASH; type-isolated from the single-key STRING). |
|
|
124
|
+
| `group_ttl_seconds` | `18000` | Group HASH TTL (idle-GC heartbeat: any group write renews it). |
|
|
125
|
+
| `socket_timeout_ms` | `1000` | Read/query path timeout. |
|
|
126
|
+
| `recv_timeout_ms` | `500` | Push path (reserve/summary write) timeout. |
|
|
127
|
+
|
|
128
|
+
Strategy-level knobs live on `RedisDedupCarrierConfig`
|
|
129
|
+
(`fail_closed_on_unavailable`, `cold_path_max_concurrency`, gate-full /
|
|
130
|
+
unavailable `retry_after` seconds, `log_context` mapping) and on
|
|
131
|
+
`RedisGroupDedupCarrierConfig` (`group_cold_path_max_concurrency`, same
|
|
132
|
+
family of knobs).
|
|
133
|
+
|
|
134
|
+
### `contrib.sql_upsert.DbConfig`
|
|
135
|
+
|
|
136
|
+
| Field | Default | Description |
|
|
137
|
+
|-------|---------|-------------|
|
|
138
|
+
| `connection_string` | **required** | SQLAlchemy async URL (`sqlite+aiosqlite:///...` or `mssql+aioodbc://...`). Missing ⇒ assembly error with fix instructions. |
|
|
139
|
+
| `echo` | `false` | SQLAlchemy echo logging. |
|
|
140
|
+
| `write_timeout_seconds` | `20` | Driver statement timeout (MSSQL/pyodbc hook; must be < `write_wait_seconds`). |
|
|
141
|
+
| `write_wait_seconds` | `25` | Batch upsert call-level `wait_for` ceiling. |
|
|
142
|
+
| `pool_timeout_seconds` | `3` | Pool checkout timeout (fail fast when exhausted). |
|
|
143
|
+
| `write_pool_size` | `10` | Fixed write-pool connections. |
|
|
144
|
+
| `write_pool_max_overflow` | `20` | Write-pool overflow connections. |
|
|
145
|
+
|
|
146
|
+
The dialect (SQLite vs MSSQL) is selected by the connection string;
|
|
147
|
+
`contrib.sqlite_upsert` and `contrib.mssql_upsert` are the pre-assembled
|
|
148
|
+
factory entries (`SqliteConsumer` / `MssqlConsumer`).
|
|
149
|
+
|
|
150
|
+
### Backpressure probing (contrib.http_probe)
|
|
151
|
+
|
|
152
|
+
`HttpProbeSignal` is constructed with a `BackpressureConfig` instance (see
|
|
153
|
+
below) — build the config object yourself and pass it to the producer via
|
|
154
|
+
`ProducerOptions(signal=HttpProbeSignal(config), backpressure=config)`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## KafkaConfig (`ProducerOptions.kafka`)
|
|
159
|
+
|
|
160
|
+
The producer takes `bootstrap_servers`/`topic` as **required** flat
|
|
161
|
+
constructor arguments; the remaining connection/self-healing knobs live on
|
|
162
|
+
`KafkaConfig`, injectable via `ProducerOptions.kafka` (defaults below). The
|
|
163
|
+
constructor values always override the `bootstrap_servers` / `topic` fields.
|
|
164
|
+
|
|
165
|
+
| Field | Default | Description |
|
|
166
|
+
|-------|---------|-------------|
|
|
167
|
+
| `bootstrap_servers` | `kafka:9092` | Comma-separated broker list. `http://` / `https://` / `kafka://` scheme prefixes are stripped automatically. Always overridden by the `Producer` constructor value. |
|
|
168
|
+
| `topic` | `None` | Topic to push to (constructor value wins; validated when a raw `KafkaConfig` is used directly). |
|
|
169
|
+
| `acks` | `all` | Producer acks level. |
|
|
170
|
+
| `request_timeout_ms` | `10000` | Producer request timeout (ms). |
|
|
171
|
+
| `enable_idempotence` | `true` | Idempotent producer (safe retries). |
|
|
172
|
+
| `health_check_interval_seconds` | `30.0` | Periodic health probe interval; unhealthy instances are rebuilt. |
|
|
173
|
+
| `reconnect_base_backoff_seconds` | `1.0` | Reconnect exponential backoff base. |
|
|
174
|
+
| `reconnect_max_backoff_seconds` | `30.0` | Reconnect backoff cap. |
|
|
175
|
+
| `reconnect_failure_threshold` | `1` | Consecutive send failures before a rebuild is triggered (raise to tolerate broker jitter). |
|
|
176
|
+
| `send_failure_window_seconds` | `30.0` | A send failure within this window marks the instance unhealthy. |
|
|
177
|
+
| `unhealthy_check_interval_seconds` | `5.0` | High-frequency probe interval while unhealthy/rebuilding. |
|
|
178
|
+
|
|
179
|
+
The dead-letter topic is configured on the consumer side via
|
|
180
|
+
`DlqOptions.topic` (see above) — no longer on `KafkaConfig`.
|
|
181
|
+
|
|
182
|
+
## BackpressureConfig (`ProducerOptions.backpressure`)
|
|
183
|
+
|
|
184
|
+
The producer defaults to the built-in `ManualBackpressureSignal` (static
|
|
185
|
+
switch, backpressure off). For dynamic probing pass
|
|
186
|
+
`options=ProducerOptions(backpressure=..., signal=...)` —
|
|
187
|
+
`contrib.http_probe.HttpProbeSignal` reads this config object.
|
|
188
|
+
|
|
189
|
+
| Field | Default | Description |
|
|
190
|
+
|-------|---------|-------------|
|
|
191
|
+
| `enabled` | `true` | Master switch; `false` = fully open (emergency rollback). |
|
|
192
|
+
| `consumer_health_url` | `http://localhost:9109/health` | Consumer health endpoint polled by the probe. |
|
|
193
|
+
| `check_interval_seconds` | `30.0` | Poll period while OPEN. |
|
|
194
|
+
| `timeout_seconds` | `2.0` | Per-probe timeout. |
|
|
195
|
+
| `probe_retries` | `3` | Retries per cycle (excluding the first attempt); `0` disables. |
|
|
196
|
+
| `probe_retry_interval_seconds` | `15.0` | Interval between retries. |
|
|
197
|
+
| `trip_seconds` | `9000.0` | Reject when backlog age exceeds this. |
|
|
198
|
+
| `recover_seconds` | `7200.0` | Resume when backlog age falls below this (hysteresis anti-flapping). |
|
|
199
|
+
| `retry_after_seconds` | `60` | `Retry-After` hint (seconds) while rejecting. |
|
|
200
|
+
| `fail_closed_on_unreachable` | `true` | Treat unreachable probe (retries exhausted) as backlog-exceeded. |
|
|
201
|
+
| `reject_on_any_degraded` | `false` | Legacy escape hatch: reject when any component is degraded. |
|
|
202
|
+
| `unhealthy_check_interval_seconds` | `5.0` | Poll period while REJECTING. |
|
|
203
|
+
|
|
204
|
+
## Metrics window
|
|
205
|
+
|
|
206
|
+
Health-snapshot rate metrics: both `Producer` and `Consumer` maintain
|
|
207
|
+
in-memory sliding windows and expose computed rates / latencies through their
|
|
208
|
+
health snapshots (`push_rate`, `handle_rate`, `produce_latency_ms_avg`,
|
|
209
|
+
...). Windowed aggregates are computed on read — no background tasks, no extra
|
|
210
|
+
endpoint; rates decay to `0.0` once traffic stops for longer than the window.
|
|
211
|
+
|
|
212
|
+
Configure via `metrics_window_seconds` on `ProducerOptions` /
|
|
213
|
+
`ConsumerOptions`:
|
|
214
|
+
|
|
215
|
+
| Value | Description |
|
|
216
|
+
|-------|-------------|
|
|
217
|
+
| `60` (default) | Sliding-window length (seconds) for all rate/latency fields. Valid range 1–600; out-of-range values fail at construction. Short windows react faster but are noisier. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: streamgate
|
|
3
|
-
Version:
|
|
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
|
|
@@ -21,7 +21,6 @@ Classifier: Topic :: System :: Distributed Computing
|
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Requires-Dist: aiokafka>=0.10
|
|
24
|
-
Requires-Dist: loguru>=0.7
|
|
25
24
|
Requires-Dist: pydantic>=2.5
|
|
26
25
|
Provides-Extra: http
|
|
27
26
|
Requires-Dist: httpx>=0.27; extra == 'http'
|
|
@@ -45,7 +44,7 @@ English | [中文](README.zh-CN.md)
|
|
|
45
44
|
|
|
46
45
|
**streamgate** is a pure-core Kafka data pipeline framework: a dedup-aware door on the way in, reliable delivery through Kafka, and one obvious outlet on the other side — `Producer(bootstrap_servers, topic, ...)` / `Consumer(bootstrap_servers, topic, group_id, handler)`.
|
|
47
46
|
|
|
48
|
-
The wheel installs exactly
|
|
47
|
+
The wheel installs exactly two dependencies (`aiokafka`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Logging goes through the standard-library `logging` module — the library emits records and never configures them. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
|
|
49
48
|
|
|
50
49
|
- 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
|
|
51
50
|
|
|
@@ -68,6 +67,8 @@ aiokafka gives you **transport**; streamgate gives you the **operating semantics
|
|
|
68
67
|
|
|
69
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/):
|
|
70
69
|
|
|
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
|
```python
|
|
72
73
|
import asyncio
|
|
73
74
|
from pydantic import BaseModel
|
|
@@ -218,7 +219,7 @@ await consumer.run()
|
|
|
218
219
|
## Installation
|
|
219
220
|
|
|
220
221
|
```bash
|
|
221
|
-
pip install streamgate # aiokafka +
|
|
222
|
+
pip install streamgate # aiokafka + pydantic. That's it.
|
|
222
223
|
pip install "streamgate[redis]" # + Redis distributed dedup carrier
|
|
223
224
|
pip install "streamgate[sql]" # + SQL upsert outlet (SQLite + MSSQL)
|
|
224
225
|
pip install "streamgate[http]" # + HTTP probe backpressure
|
|
@@ -228,7 +229,29 @@ For anything else (PostgreSQL, Elasticsearch, your own destination...), write th
|
|
|
228
229
|
|
|
229
230
|
## Configuration
|
|
230
231
|
|
|
231
|
-
|
|
232
|
+
There are exactly two sources of truth. **Required settings are true required constructor parameters** — `Producer(bootstrap_servers, topic, ...)` and `Consumer(bootstrap_servers, topic, group_id, handler)` fail with a native `TypeError` the moment an argument is missing (they are never silently defaulted or read from the environment). **Optional settings directly hold their built-in defaults** and fold into `ProducerOptions` / `ConsumerOptions` (dedup, backpressure, DLQ, tuning, metrics window) — omit them and the documented defaults apply. The library reads no environment variables; if your host wants env-driven config, resolve it yourself and pass the values explicitly (the examples do exactly that). Database/Redis connection settings belong to your application (see the examples' local configs). See [CONFIGURATION.md](CONFIGURATION.md).
|
|
233
|
+
|
|
234
|
+
## Logging
|
|
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. 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):
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import logging
|
|
244
|
+
|
|
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])
|
|
250
|
+
```
|
|
251
|
+
|
|
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).
|
|
232
255
|
|
|
233
256
|
## Examples
|
|
234
257
|
|
|
@@ -236,7 +259,7 @@ See [`examples/`](examples/) for five runnable pipelines plus a `docker-compose.
|
|
|
236
259
|
|
|
237
260
|
## Migrating from 0.x
|
|
238
261
|
|
|
239
|
-
1.0.0 redesigned **both** sides (breaking): the consumer side replaces `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` with the flat `Consumer` constructor; the producer side replaces `IngestBinding` + `IngestGateway` with the flat `Producer` constructor (`process()` → `push()`, `IngestOutcome` → `PushResult`, `overwrite` → `force`, `admission` → `DedupOptions`, `entity_key`/`slot_key` split into routing `key` + identity `DedupOptions.key`). See the [CHANGELOG](CHANGELOG.md) for the complete old→new mapping (API, result kinds, metrics names, log events,
|
|
262
|
+
1.0.0 redesigned **both** sides (breaking): the consumer side replaces `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` with the flat `Consumer` constructor; the producer side replaces `IngestBinding` + `IngestGateway` with the flat `Producer` constructor (`process()` → `push()`, `IngestOutcome` → `PushResult`, `overwrite` → `force`, `admission` → `DedupOptions`, `entity_key`/`slot_key` split into routing `key` + identity `DedupOptions.key`). 3.0.0 removed the bundled loguru logging (stdlib `logging` instead — `from streamgate import logger` is gone) **and all environment-variable fallbacks**: required settings are now true required constructor parameters and every `KAFKA__*` / `CONSUMER__*` / `METRICS__*` key is gone — options hold their built-in defaults directly. See the [CHANGELOG](CHANGELOG.md) for the complete old→new mapping (API, result kinds, metrics names, log events, import paths) and the per-key migration table.
|
|
240
263
|
|
|
241
264
|
## Roadmap
|
|
242
265
|
|
|
@@ -9,7 +9,7 @@ English | [中文](README.zh-CN.md)
|
|
|
9
9
|
|
|
10
10
|
**streamgate** is a pure-core Kafka data pipeline framework: a dedup-aware door on the way in, reliable delivery through Kafka, and one obvious outlet on the other side — `Producer(bootstrap_servers, topic, ...)` / `Consumer(bootstrap_servers, topic, group_id, handler)`.
|
|
11
11
|
|
|
12
|
-
The wheel installs exactly
|
|
12
|
+
The wheel installs exactly two dependencies (`aiokafka`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Logging goes through the standard-library `logging` module — the library emits records and never configures them. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
|
|
13
13
|
|
|
14
14
|
- 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
|
|
15
15
|
|
|
@@ -32,6 +32,8 @@ 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`); 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
|
+
|
|
35
37
|
```python
|
|
36
38
|
import asyncio
|
|
37
39
|
from pydantic import BaseModel
|
|
@@ -182,7 +184,7 @@ await consumer.run()
|
|
|
182
184
|
## Installation
|
|
183
185
|
|
|
184
186
|
```bash
|
|
185
|
-
pip install streamgate # aiokafka +
|
|
187
|
+
pip install streamgate # aiokafka + pydantic. That's it.
|
|
186
188
|
pip install "streamgate[redis]" # + Redis distributed dedup carrier
|
|
187
189
|
pip install "streamgate[sql]" # + SQL upsert outlet (SQLite + MSSQL)
|
|
188
190
|
pip install "streamgate[http]" # + HTTP probe backpressure
|
|
@@ -192,7 +194,29 @@ For anything else (PostgreSQL, Elasticsearch, your own destination...), write th
|
|
|
192
194
|
|
|
193
195
|
## Configuration
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
There are exactly two sources of truth. **Required settings are true required constructor parameters** — `Producer(bootstrap_servers, topic, ...)` and `Consumer(bootstrap_servers, topic, group_id, handler)` fail with a native `TypeError` the moment an argument is missing (they are never silently defaulted or read from the environment). **Optional settings directly hold their built-in defaults** and fold into `ProducerOptions` / `ConsumerOptions` (dedup, backpressure, DLQ, tuning, metrics window) — omit them and the documented defaults apply. The library reads no environment variables; if your host wants env-driven config, resolve it yourself and pass the values explicitly (the examples do exactly that). Database/Redis connection settings belong to your application (see the examples' local configs). See [CONFIGURATION.md](CONFIGURATION.md).
|
|
198
|
+
|
|
199
|
+
## Logging
|
|
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. 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):
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import logging
|
|
209
|
+
|
|
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])
|
|
215
|
+
```
|
|
216
|
+
|
|
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).
|
|
196
220
|
|
|
197
221
|
## Examples
|
|
198
222
|
|
|
@@ -200,7 +224,7 @@ See [`examples/`](examples/) for five runnable pipelines plus a `docker-compose.
|
|
|
200
224
|
|
|
201
225
|
## Migrating from 0.x
|
|
202
226
|
|
|
203
|
-
1.0.0 redesigned **both** sides (breaking): the consumer side replaces `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` with the flat `Consumer` constructor; the producer side replaces `IngestBinding` + `IngestGateway` with the flat `Producer` constructor (`process()` → `push()`, `IngestOutcome` → `PushResult`, `overwrite` → `force`, `admission` → `DedupOptions`, `entity_key`/`slot_key` split into routing `key` + identity `DedupOptions.key`). See the [CHANGELOG](CHANGELOG.md) for the complete old→new mapping (API, result kinds, metrics names, log events,
|
|
227
|
+
1.0.0 redesigned **both** sides (breaking): the consumer side replaces `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` with the flat `Consumer` constructor; the producer side replaces `IngestBinding` + `IngestGateway` with the flat `Producer` constructor (`process()` → `push()`, `IngestOutcome` → `PushResult`, `overwrite` → `force`, `admission` → `DedupOptions`, `entity_key`/`slot_key` split into routing `key` + identity `DedupOptions.key`). 3.0.0 removed the bundled loguru logging (stdlib `logging` instead — `from streamgate import logger` is gone) **and all environment-variable fallbacks**: required settings are now true required constructor parameters and every `KAFKA__*` / `CONSUMER__*` / `METRICS__*` key is gone — options hold their built-in defaults directly. See the [CHANGELOG](CHANGELOG.md) for the complete old→new mapping (API, result kinds, metrics names, log events, import paths) and the per-key migration table.
|
|
204
228
|
|
|
205
229
|
## Roadmap
|
|
206
230
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
判重、背压、自愈 批量缓冲、重试、坏数据隔离
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
框架核心只装
|
|
17
|
+
框架核心只装 2 个依赖(`aiokafka` / `pydantic`),核心层**没有一行数据库、Redis、HTTP 客户端代码**。日志走标准库 `logging`——库只发日志记录、永不配置。官方 I/O 策略实现收录在 [`streamgate.contrib`](#不想自己写用-contrib)——按 extras 按需安装、装完即可 import;可运行的完整演示在 [`examples/`](examples/)。
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -358,7 +358,7 @@ await consumer.run()
|
|
|
358
358
|
## 安装
|
|
359
359
|
|
|
360
360
|
```bash
|
|
361
|
-
pip install streamgate # 只装 aiokafka +
|
|
361
|
+
pip install streamgate # 只装 aiokafka + pydantic
|
|
362
362
|
pip install "streamgate[redis]" # + Redis 分布式判重
|
|
363
363
|
pip install "streamgate[sql]" # + SQL 幂等出口(SQLite + MSSQL)
|
|
364
364
|
pip install "streamgate[http]" # + HTTP 探活背压
|
|
@@ -368,7 +368,29 @@ pip install "streamgate[http]" # + HTTP 探活背压
|
|
|
368
368
|
|
|
369
369
|
## 配置
|
|
370
370
|
|
|
371
|
-
|
|
371
|
+
配置只有两个真相来源:**必填项是真必填构造参数**——`Producer(bootstrap_servers, topic, ...)` 与 `Consumer(bootstrap_servers, topic, group_id, handler)` 缺任一参数即原生 `TypeError`(绝不静默默认、绝不读环境变量);**可选项直接持有内置默认值**,收口在 `ProducerOptions` / `ConsumerOptions`(判重、背压、DLQ、调优、指标窗口等),不传即文档默认行为。库代码不读取任何环境变量;宿主若要 env 驱动配置,自行解析后显式传参(示例即如此)。数据库/Redis 连接配置归你的应用管(示例各自带本地配置)。详见 [CONFIGURATION.md](CONFIGURATION.md)。
|
|
372
|
+
|
|
373
|
+
## 日志
|
|
374
|
+
|
|
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` 即可让全部结构化字段可见(所有示例均如此):
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
import logging
|
|
383
|
+
|
|
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])
|
|
389
|
+
```
|
|
390
|
+
|
|
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)。
|
|
372
394
|
|
|
373
395
|
## 架构
|
|
374
396
|
|
|
@@ -396,7 +418,7 @@ pip install "streamgate[http]" # + HTTP 探活背压
|
|
|
396
418
|
|
|
397
419
|
## 从 0.x 迁移
|
|
398
420
|
|
|
399
|
-
1.0.0 对**两端**都做了破坏性重设计:消费侧 `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` 由扁平 `Consumer` 构造器替代;生产侧 `IngestBinding` + `IngestGateway` 由扁平 `Producer` 构造器替代(`process()` → `push()`、`IngestOutcome` → `PushResult`、`overwrite` → `force`、`admission` → `DedupOptions`、`entity_key`/`slot_key` 拆分为路由键 `key` + 身份键 `DedupOptions.key
|
|
421
|
+
1.0.0 对**两端**都做了破坏性重设计:消费侧 `ConsumerWorker` + `ConsumeSpec` + `RecordWriter` 由扁平 `Consumer` 构造器替代;生产侧 `IngestBinding` + `IngestGateway` 由扁平 `Producer` 构造器替代(`process()` → `push()`、`IngestOutcome` → `PushResult`、`overwrite` → `force`、`admission` → `DedupOptions`、`entity_key`/`slot_key` 拆分为路由键 `key` + 身份键 `DedupOptions.key`)。3.0.0 移除了内置 loguru 日志(改走标准库 `logging`,`from streamgate import logger` 不复存在),**并移除了全部环境变量回退**:必填项成为真必填构造参数,所有 `KAFKA__*` / `CONSUMER__*` / `METRICS__*` 键不复存在——可选项字段直接持有内置默认值。完整的新旧对照(API、结果 kind、指标名、日志事件、import 路径)与逐项迁移表见 [CHANGELOG](CHANGELOG.md)。
|
|
400
422
|
|
|
401
423
|
## 路线图
|
|
402
424
|
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import logging
|
|
11
12
|
import os
|
|
12
13
|
|
|
13
14
|
from health_server import serve_consumer_health
|
|
14
15
|
|
|
15
|
-
from streamgate import ConsumeContext, Consumer, JsonObject
|
|
16
|
+
from streamgate import ConsumeContext, Consumer, JsonFormatter, JsonObject
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
async def print_orders(
|
|
@@ -23,6 +24,9 @@ async def print_orders(
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
async def main() -> None:
|
|
27
|
+
handler = logging.StreamHandler()
|
|
28
|
+
handler.setFormatter(JsonFormatter())
|
|
29
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
|
26
30
|
consumer = Consumer(
|
|
27
31
|
bootstrap_servers=os.environ.get("KAFKA__BOOTSTRAP_SERVERS", "localhost:29092"),
|
|
28
32
|
topic=os.environ.get("KAFKA__TOPIC", "orders"),
|