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.
Files changed (84) hide show
  1. {streamgate-2.1.0 → streamgate-3.1.0}/.github/workflows/ci.yml +1 -1
  2. {streamgate-2.1.0 → streamgate-3.1.0}/CHANGELOG.md +144 -0
  3. streamgate-3.1.0/CONFIGURATION.md +217 -0
  4. {streamgate-2.1.0 → streamgate-3.1.0}/PKG-INFO +29 -6
  5. {streamgate-2.1.0 → streamgate-3.1.0}/README.md +28 -4
  6. {streamgate-2.1.0 → streamgate-3.1.0}/README.zh-CN.md +26 -4
  7. {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/consume.py +5 -1
  8. {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/health_server.py +6 -3
  9. {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/produce.py +5 -1
  10. {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/consume.py +5 -1
  11. {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/health_server.py +6 -3
  12. {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/produce.py +11 -1
  13. {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/consume.py +5 -0
  14. {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/produce.py +5 -1
  15. {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/produce.py +5 -1
  16. {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/consume.py +5 -1
  17. {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/produce.py +5 -1
  18. {streamgate-2.1.0 → streamgate-3.1.0}/pyproject.toml +2 -2
  19. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/__init__.py +6 -6
  20. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/config.py +8 -7
  21. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/dlq.py +12 -12
  22. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/loop.py +35 -32
  23. streamgate-3.1.0/src/streamgate/consumer/options.py +123 -0
  24. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/runner.py +32 -70
  25. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/probe_signal.py +20 -16
  26. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/mssql_upsert/__init__.py +11 -6
  27. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/cache.py +8 -5
  28. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/carrier.py +16 -13
  29. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_cache.py +7 -4
  30. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/group_carrier.py +19 -16
  31. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/backfill.py +8 -5
  32. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/engines.py +6 -2
  33. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/factory.py +11 -5
  34. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/upsert.py +12 -9
  35. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sqlite_upsert/__init__.py +11 -6
  36. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/producer.py +44 -82
  37. streamgate-3.1.0/src/streamgate/obs/__init__.py +9 -0
  38. streamgate-3.1.0/src/streamgate/obs/formatter.py +57 -0
  39. streamgate-3.1.0/src/streamgate/obs/logging.py +39 -0
  40. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/obs/metrics.py +3 -2
  41. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/resilience/health.py +5 -2
  42. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/transport/codec.py +6 -3
  43. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/transport/kafka.py +16 -18
  44. {streamgate-2.1.0 → streamgate-3.1.0}/uv.lock +2 -33
  45. streamgate-2.1.0/CONFIGURATION.md +0 -190
  46. streamgate-2.1.0/src/streamgate/consumer/options.py +0 -261
  47. streamgate-2.1.0/src/streamgate/obs/logging.py +0 -43
  48. streamgate-2.1.0/src/streamgate/transport/__init__.py +0 -0
  49. {streamgate-2.1.0 → streamgate-3.1.0}/.github/workflows/release.yml +0 -0
  50. {streamgate-2.1.0 → streamgate-3.1.0}/.gitignore +0 -0
  51. {streamgate-2.1.0 → streamgate-3.1.0}/AGENTS.md +0 -0
  52. {streamgate-2.1.0 → streamgate-3.1.0}/CONTRIBUTING.md +0 -0
  53. {streamgate-2.1.0 → streamgate-3.1.0}/LICENSE +0 -0
  54. {streamgate-2.1.0 → streamgate-3.1.0}/examples/docker-compose.yml +0 -0
  55. {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/README.md +0 -0
  56. {streamgate-2.1.0 → streamgate-3.1.0}/examples/http_probe/models.py +0 -0
  57. {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/README.md +0 -0
  58. {streamgate-2.1.0 → streamgate-3.1.0}/examples/prod_pipeline/models.py +0 -0
  59. {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/README.md +0 -0
  60. {streamgate-2.1.0 → streamgate-3.1.0}/examples/pure_pipeline/models.py +0 -0
  61. {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/README.md +0 -0
  62. {streamgate-2.1.0 → streamgate-3.1.0}/examples/redis_admission/models.py +0 -0
  63. {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/README.md +0 -0
  64. {streamgate-2.1.0 → streamgate-3.1.0}/examples/sqlite_upsert/models.py +0 -0
  65. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/__init__.py +0 -0
  66. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/consumer/classifier.py +0 -0
  67. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/__init__.py +0 -0
  68. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/_deps.py +0 -0
  69. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
  70. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/__init__.py +0 -0
  71. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/redis_dedup/config.py +0 -0
  72. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/__init__.py +0 -0
  73. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py +0 -0
  74. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/mssql.py +0 -0
  75. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/_dialects/sqlite.py +0 -0
  76. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/classifier.py +0 -0
  77. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/contrib/sql_upsert/config.py +0 -0
  78. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/__init__.py +0 -0
  79. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/__init__.py +0 -0
  80. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/ingest/dedup/in_memory.py +0 -0
  81. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/protocols.py +0 -0
  82. {streamgate-2.1.0/src/streamgate/obs → streamgate-3.1.0/src/streamgate/resilience}/__init__.py +0 -0
  83. {streamgate-2.1.0 → streamgate-3.1.0}/src/streamgate/resilience/backpressure.py +0 -0
  84. {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/loguru/pydantic (+transitive),
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: 2.1.0
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 three dependencies (`aiokafka`, `loguru`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
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 + loguru + pydantic. That's it.
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
- Both sides take required settings as flat constructor arguments with environment-variable fallbacks (`KAFKA__BOOTSTRAP_SERVERS`, `KAFKA__TOPIC`, `CONSUMER__GROUP_ID`); advanced knobs fold into `ProducerOptions` / `ConsumerOptions` (dedup, backpressure, DLQ, tuning, metrics window) with the same env fallbacks. Required settings fail fast at startup with fix instructions. Database/Redis connection settings belong to your application (see the examples' local configs). See [CONFIGURATION.md](CONFIGURATION.md).
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, env keys, import paths).
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 three dependencies (`aiokafka`, `loguru`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
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 + loguru + pydantic. That's it.
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
- Both sides take required settings as flat constructor arguments with environment-variable fallbacks (`KAFKA__BOOTSTRAP_SERVERS`, `KAFKA__TOPIC`, `CONSUMER__GROUP_ID`); advanced knobs fold into `ProducerOptions` / `ConsumerOptions` (dedup, backpressure, DLQ, tuning, metrics window) with the same env fallbacks. Required settings fail fast at startup with fix instructions. Database/Redis connection settings belong to your application (see the examples' local configs). See [CONFIGURATION.md](CONFIGURATION.md).
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, env keys, import paths).
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
- 框架核心只装 3 个依赖(`aiokafka` / `loguru` / `pydantic`),核心层**没有一行数据库、Redis、HTTP 客户端代码**。官方 I/O 策略实现收录在 [`streamgate.contrib`](#不想自己写用-contrib)——按 extras 按需安装、装完即可 import;可运行的完整演示在 [`examples/`](examples/)。
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 + loguru + pydantic
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
- `Producer` / `Consumer` 必填项直接传参,未传时回退同名环境变量(`KAFKA__BOOTSTRAP_SERVERS` / `KAFKA__TOPIC` / `CONSUMER__GROUP_ID`);高级项收口在 `ProducerOptions` / `ConsumerOptions`(判重、背压、DLQ、调优、指标窗口等),同样跟随 `KAFKA__*` / `CONSUMER__*` 环境变量回退。必填项缺失即启动失败并附修复指引,绝不带猜测默认值上路。数据库/Redis 连接配置归你的应用管(示例各自带本地配置)。详见 [CONFIGURATION.md](CONFIGURATION.md)。
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`)。完整的新旧对照(API、结果 kind、指标名、日志事件、环境变量、import 路径)见 [CHANGELOG](CHANGELOG.md)。
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"),