mammoth 1.4.0 → 1.5.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +69 -19
- data/config/mammoth.example.yml +8 -3
- data/config/mammoth.schema.json +3 -3
- data/lib/mammoth/application.rb +32 -5
- data/lib/mammoth/cli.rb +1 -1
- data/lib/mammoth/delivery_progress_coordinator.rb +5 -2
- data/lib/mammoth/delivery_worker.rb +32 -11
- data/lib/mammoth/lifecycle_hooks.rb +3 -0
- data/lib/mammoth/logging.rb +107 -0
- data/lib/mammoth/observability_server.rb +11 -2
- data/lib/mammoth/operational_state/sqlite_adapter.rb +1 -1
- data/lib/mammoth/payload_policy.rb +5 -0
- data/lib/mammoth/sources/postgres.rb +16 -3
- data/lib/mammoth/sources/postgres_publication_inspector.rb +1 -0
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +1 -0
- metadata +27 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d7fd1f170a51138c5f3f1b8cc31c388428dd9052020f38653242c774bc6a92e
|
|
4
|
+
data.tar.gz: 134f6734ff5a1c377ba318a924630f6792dd51f66b0510ca37157836caacc993
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7421871882b9288892ee4c3c2e3e0701839dd60165b238b771a98cd2ec21aa0ed1323b4605ce1f1c9270cd711430aae97f61b0e5f7edd5316a6b77a8686e1f39
|
|
7
|
+
data.tar.gz: 6b9d480c5c71beadbbf2633597e6af88176764b1a8e44e49b41cfa24c4636e0f14a08287f2522ce8bf885b0b7b95930600836697cd66acefb67452e5e2c3f2b5
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
## 1.5.1 - 2026-07-25
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Adopt Mammoth as the ecosystem brand, identify this open-source project as
|
|
14
|
+
Mammoth Data Plane, and align documentation, published gem metadata,
|
|
15
|
+
configuration descriptions, and branding assets with the new hierarchy.
|
|
16
|
+
|
|
17
|
+
## 1.5.0 - 2026-07-24
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Default the unauthenticated observability HTTP server to the loopback
|
|
22
|
+
interface and document the network controls required for non-loopback
|
|
23
|
+
bindings.
|
|
24
|
+
- Implement `logging.level` with container-friendly JSON stdout logs for
|
|
25
|
+
application and replication lifecycle, delivery outcomes, retries, dead
|
|
26
|
+
letters, checkpoints, and debug-level work progress.
|
|
27
|
+
- Require a patched `json` gem and remove the vulnerable bundled default-gem
|
|
28
|
+
specification from the runtime image to address CVE-2026-33210.
|
|
29
|
+
|
|
30
|
+
### Quality
|
|
31
|
+
|
|
32
|
+
- Run the container quickstart smoke test on pull requests and whenever Docker,
|
|
33
|
+
application, configuration, or dependency inputs change.
|
|
34
|
+
- Added Dependabot updates, Ruby dependency auditing, dependency review,
|
|
35
|
+
CodeQL analysis, and high- and critical-severity container vulnerability
|
|
36
|
+
scanning.
|
|
37
|
+
- Pin the Ruby container base image to an immutable digest and publish release
|
|
38
|
+
images with SBOM and maximum provenance attestations.
|
|
39
|
+
- Published a full reference benchmark snapshot with machine-readable results,
|
|
40
|
+
raw output, and documented interpretation across projection, delivery,
|
|
41
|
+
fanout, SQLite operational state, observability, and dead-letter replay.
|
|
42
|
+
|
|
7
43
|
## 1.4.0 - 2026-07-19
|
|
8
44
|
|
|
9
45
|
### Added
|
data/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
[](https://rubygems.org/gems/mammoth)
|
|
18
18
|
[](https://www.ruby-lang.org/)
|
|
19
19
|
[](https://github.com/kanutocd/mammoth/actions/workflows/ci.yml)
|
|
20
|
+
[](https://github.com/kanutocd/mammoth/actions/workflows/security.yml)
|
|
20
21
|
[](https://github.com/kanutocd/mammoth/blob/main/.github/workflows/ci.yml#L50)
|
|
21
22
|
[](LICENSE.txt)
|
|
22
23
|
|
|
@@ -24,6 +25,17 @@
|
|
|
24
25
|
Mammoth is a self-hosted PostgreSQL event relay focused on reliable delivery
|
|
25
26
|
of database change events.
|
|
26
27
|
|
|
28
|
+
This repository contains the **Mammoth Data Plane**, the open-source runtime in
|
|
29
|
+
the broader Mammoth ecosystem:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
Mammoth
|
|
33
|
+
├── Mammoth Data Plane
|
|
34
|
+
├── Mammoth Control Plane
|
|
35
|
+
├── Mammoth Control Agent
|
|
36
|
+
└── Mammoth Extensions
|
|
37
|
+
```
|
|
38
|
+
|
|
27
39
|
```text
|
|
28
40
|
PostgreSQL
|
|
29
41
|
↓
|
|
@@ -31,7 +43,7 @@ CDC Ecosystem source adapter
|
|
|
31
43
|
↓
|
|
32
44
|
CDC::Core::TransactionEnvelope
|
|
33
45
|
↓
|
|
34
|
-
Mammoth
|
|
46
|
+
Mammoth Data Plane
|
|
35
47
|
↓
|
|
36
48
|
Webhook fanout
|
|
37
49
|
```
|
|
@@ -106,6 +118,7 @@ Mammoth 1.x includes:
|
|
|
106
118
|
- per-destination payload removal and masking policies
|
|
107
119
|
- per-destination enable/disable and retry policy controls
|
|
108
120
|
- delivery worker with retry, delivered-ledger, and DLQ handling
|
|
121
|
+
- configurable `debug`, `info`, `warn`, and `error` JSON logging to stdout
|
|
109
122
|
- contiguous delivery watermark for checkpoint and PostgreSQL acknowledgement
|
|
110
123
|
- source-owned transport LSN preservation independent of payload `commit_lsn`
|
|
111
124
|
- fail-closed PostgreSQL slot and checkpoint continuity preflight
|
|
@@ -196,7 +209,7 @@ payload policy.
|
|
|
196
209
|
|
|
197
210
|
## Extensions
|
|
198
211
|
|
|
199
|
-
Mammoth
|
|
212
|
+
Mammoth Data Plane exposes small adapter registries for future extensions:
|
|
200
213
|
|
|
201
214
|
- operational state adapters
|
|
202
215
|
- destination adapters
|
|
@@ -257,6 +270,30 @@ bundle exec ./exe/mammoth start config/mammoth.example.yml
|
|
|
257
270
|
bundle exec ./exe/mammoth observability config/mammoth.example.yml
|
|
258
271
|
```
|
|
259
272
|
|
|
273
|
+
## Logging
|
|
274
|
+
|
|
275
|
+
Mammoth emits one JSON object per line to standard output. Set
|
|
276
|
+
`logging.level` to `debug`, `info`, `warn`, or `error`; `info` is the
|
|
277
|
+
recommended normal setting, while `debug` adds per-work and WAL acknowledgement
|
|
278
|
+
records.
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
logging:
|
|
282
|
+
level: info
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
In Docker Compose, follow the application, replication, delivery, retry,
|
|
286
|
+
dead-letter, and checkpoint activity with:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
docker compose logs -f mammoth
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Operational identifiers are included, but payload bodies, configured headers,
|
|
293
|
+
credentials, signing secrets, and exception messages are excluded. See
|
|
294
|
+
[`docs/CONFIGURATION.md`](docs/CONFIGURATION.md#logging) for the event and level
|
|
295
|
+
details.
|
|
296
|
+
|
|
260
297
|
Reconstruct and deliver a single persisted event JSON file through Mammoth's
|
|
261
298
|
core delivery path:
|
|
262
299
|
|
|
@@ -281,22 +318,35 @@ rather than opening SQLite directly.
|
|
|
281
318
|
|
|
282
319
|
## Performance
|
|
283
320
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
321
|
+
A reference benchmark run on Ruby 4.0.5 produced:
|
|
322
|
+
|
|
323
|
+
| Benchmark | Result |
|
|
324
|
+
| --------------------------------------------------- | --------------------: |
|
|
325
|
+
| Event payload projection with explicit ID | **56,265 events/sec** |
|
|
326
|
+
| Transaction payload projection with explicit IDs | **55,612 events/sec** |
|
|
327
|
+
| Mask payload-policy projection | **72,403 events/sec** |
|
|
328
|
+
| Concurrent delivery, 25 workers | **3,825 events/sec** |
|
|
329
|
+
| Concurrent delivery, 50 workers | **7,447 events/sec** |
|
|
330
|
+
| Signed webhook delivery with 10 ms receiver latency | **83.5 requests/sec** |
|
|
331
|
+
| SQLite delivered-ledger writes | **1,630 writes/sec** |
|
|
332
|
+
| Dead-letter replay mechanics | **1,030 entries/sec** |
|
|
333
|
+
|
|
334
|
+
At concurrency 50 with a synthetic 25 ms destination, Mammoth processed
|
|
335
|
+
**1,862 transactions/sec** against a theoretical ceiling of 2,000, while
|
|
336
|
+
maintaining **28.4 ms P95 latency**. This indicates low scheduling overhead for
|
|
337
|
+
I/O-bound downstream delivery.
|
|
338
|
+
|
|
339
|
+
These are local, single-trial reference measurements, not universal capacity
|
|
340
|
+
claims or comparisons with other CDC platforms. The benchmarks isolate specific
|
|
341
|
+
Mammoth components and may exclude PostgreSQL transport, decoding, checkpoint
|
|
342
|
+
coordination, network latency, or destination processing.
|
|
343
|
+
|
|
344
|
+
The [reference snapshot](https://github.com/kanutocd/mammoth/blob/main/benchmark/results/20260724T104950Z/snapshot.md)
|
|
345
|
+
records the exact commands, environment, raw output, and detailed
|
|
346
|
+
interpretation. Re-run the scripts in
|
|
347
|
+
[`benchmark/`](https://github.com/kanutocd/mammoth/tree/main/benchmark)
|
|
348
|
+
on your own hardware when choosing `runtime.concurrency`, destination count,
|
|
349
|
+
SQLite storage, scrape frequency, and DLQ replay expectations.
|
|
300
350
|
|
|
301
351
|
Create a publishable benchmark snapshot with:
|
|
302
352
|
|
|
@@ -352,4 +402,4 @@ building a writable database copy. See
|
|
|
352
402
|
|
|
353
403
|
## License
|
|
354
404
|
|
|
355
|
-
Mammoth OSS is licensed under the [MIT License](LICENSE.txt).
|
|
405
|
+
Mammoth OSS Data Plane is licensed under the [MIT License](LICENSE.txt).
|
data/config/mammoth.example.yml
CHANGED
|
@@ -380,12 +380,16 @@ sqlite:
|
|
|
380
380
|
path: data/mammoth.db
|
|
381
381
|
|
|
382
382
|
operational_state:
|
|
383
|
-
# Mammoth
|
|
383
|
+
# Mammoth Data Plane 0.7.x ships the sqlite operational state adapter.
|
|
384
384
|
adapter: sqlite
|
|
385
385
|
|
|
386
386
|
observability:
|
|
387
387
|
# Bind host for the optional health, readiness, and metrics server.
|
|
388
|
-
|
|
388
|
+
#
|
|
389
|
+
# The endpoints do not authenticate clients and may expose operational
|
|
390
|
+
# details. Keep the loopback default unless a trusted network boundary or
|
|
391
|
+
# authenticated reverse proxy protects the listener.
|
|
392
|
+
host: 127.0.0.1
|
|
389
393
|
|
|
390
394
|
# HTTP port for /healthz, /readyz, and /metrics.
|
|
391
395
|
port: 9393
|
|
@@ -397,7 +401,8 @@ logging:
|
|
|
397
401
|
# Very chatty; useful while diagnosing delivery or replication behavior.
|
|
398
402
|
#
|
|
399
403
|
# info:
|
|
400
|
-
# Recommended normal default.
|
|
404
|
+
# Recommended normal default. Emits lifecycle, delivery, checkpoint, retry,
|
|
405
|
+
# and dead-letter JSON records to stdout for container log collection.
|
|
401
406
|
#
|
|
402
407
|
# warn/error:
|
|
403
408
|
# Quieter production modes.
|
data/config/mammoth.schema.json
CHANGED
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"description": "Operator-defined node metadata."
|
|
159
159
|
}
|
|
160
160
|
},
|
|
161
|
-
"description": "Optional local node identity. Mammoth
|
|
161
|
+
"description": "Optional local node identity. Mammoth Data Plane uses this for status and future extension surfaces only."
|
|
162
162
|
},
|
|
163
163
|
"postgres": {
|
|
164
164
|
"type": "object",
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
"enum": [
|
|
347
347
|
"webhook"
|
|
348
348
|
],
|
|
349
|
-
"description": "Destination adapter type. Mammoth
|
|
349
|
+
"description": "Destination adapter type. Mammoth Data Plane 0.7.x supports webhook destinations."
|
|
350
350
|
},
|
|
351
351
|
"enabled": {
|
|
352
352
|
"type": "boolean",
|
|
@@ -520,7 +520,7 @@
|
|
|
520
520
|
"enum": [
|
|
521
521
|
"sqlite"
|
|
522
522
|
],
|
|
523
|
-
"description": "Operational state adapter. Mammoth
|
|
523
|
+
"description": "Operational state adapter. Mammoth Data Plane 0.7.x ships the sqlite adapter."
|
|
524
524
|
}
|
|
525
525
|
},
|
|
526
526
|
"description": "Operational state adapter selection. SQLite remains the default OSS adapter."
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Mammoth
|
|
|
12
12
|
# rubocop:disable Metrics/ClassLength
|
|
13
13
|
class Application
|
|
14
14
|
attr_reader :config, :state_adapter, :consumer, :delivery_worker, :checkpoint_store, :lifecycle_hooks, :observer,
|
|
15
|
-
:progress_coordinator
|
|
15
|
+
:progress_coordinator, :logger
|
|
16
16
|
|
|
17
17
|
# @param config [Mammoth::Configuration] loaded configuration
|
|
18
18
|
# @param source [#each, nil] injectable event source for tests and demos
|
|
@@ -21,9 +21,11 @@ module Mammoth
|
|
|
21
21
|
# @param sleeper [#call] retry sleep strategy
|
|
22
22
|
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
23
23
|
# @param observer [CDC::Core::Observer, nil] dispatch lifecycle observer
|
|
24
|
+
# @param logger [Mammoth::Logging::Logger, nil] structured operational logger
|
|
24
25
|
def initialize(config, source: nil, sink: nil, state_adapter: nil, sleeper: Kernel.method(:sleep),
|
|
25
|
-
lifecycle_hooks: LifecycleHooks.new, observer: nil)
|
|
26
|
+
lifecycle_hooks: LifecycleHooks.new, observer: nil, logger: nil)
|
|
26
27
|
@config = config
|
|
28
|
+
@logger = logger || Logging.build(config)
|
|
27
29
|
@lifecycle_hooks = build_lifecycle_hooks(lifecycle_hooks)
|
|
28
30
|
@observer = observer || MetricsObserver.new
|
|
29
31
|
@state_adapter = state_adapter || build_state_adapter
|
|
@@ -49,22 +51,37 @@ module Mammoth
|
|
|
49
51
|
# Start the application runtime and deliver consumed CDC work.
|
|
50
52
|
#
|
|
51
53
|
# @return [Integer] number of processed work units
|
|
54
|
+
# rubocop:disable Metrics/AbcSize
|
|
52
55
|
def start
|
|
53
56
|
runtime = build_runtime
|
|
54
57
|
processed = nil
|
|
55
58
|
|
|
59
|
+
operational_logger.info("application_started", mammoth_name: config&.dig("mammoth", "name"),
|
|
60
|
+
runtime: config&.dig("runtime", "adapter"),
|
|
61
|
+
delivery_unit: config&.dig("delivery", "unit"))
|
|
56
62
|
lifecycle_hooks.call(:before_start, application_context(runtime: runtime))
|
|
57
63
|
processed = process_consumer(runtime)
|
|
58
64
|
lifecycle_hooks.call(:after_start, application_context(runtime: runtime, processed: processed))
|
|
65
|
+
operational_logger.info("application_completed", mammoth_name: config&.dig("mammoth", "name"), processed:)
|
|
59
66
|
processed
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
operational_logger.error("application_failed", mammoth_name: config&.dig("mammoth", "name"),
|
|
69
|
+
error_class: e.class.name)
|
|
70
|
+
raise
|
|
60
71
|
ensure
|
|
61
72
|
lifecycle_hooks.call(:before_shutdown, application_context(runtime: runtime, processed: processed))
|
|
62
73
|
runtime.shutdown if runtime.respond_to?(:shutdown)
|
|
63
74
|
lifecycle_hooks.call(:after_shutdown, application_context(runtime: runtime, processed: processed))
|
|
75
|
+
operational_logger.info("application_stopped", mammoth_name: config&.dig("mammoth", "name"), processed:)
|
|
64
76
|
end
|
|
77
|
+
# rubocop:enable Metrics/AbcSize
|
|
65
78
|
|
|
66
79
|
private
|
|
67
80
|
|
|
81
|
+
def operational_logger
|
|
82
|
+
logger || Logging::NullLogger::INSTANCE
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
def build_lifecycle_hooks(hooks)
|
|
69
86
|
return hooks if hooks.is_a?(LifecycleHooks)
|
|
70
87
|
|
|
@@ -84,6 +101,7 @@ module Mammoth
|
|
|
84
101
|
processed = 0
|
|
85
102
|
|
|
86
103
|
consumer.start_with_boundaries do |work, group_end|
|
|
104
|
+
operational_logger.debug("work_received", work_type: work.class.name, work_id: work_identifier(work), group_end:)
|
|
87
105
|
progress_coordinator.register(work, group_end:)
|
|
88
106
|
process_work(runtime, work)
|
|
89
107
|
processed += 1
|
|
@@ -94,6 +112,13 @@ module Mammoth
|
|
|
94
112
|
processed
|
|
95
113
|
end
|
|
96
114
|
|
|
115
|
+
def work_identifier(work)
|
|
116
|
+
return work.transaction_id if work.respond_to?(:transaction_id)
|
|
117
|
+
return work.event_id if work.respond_to?(:event_id)
|
|
118
|
+
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
97
122
|
def process_work(runtime, work)
|
|
98
123
|
runtime.process(work)
|
|
99
124
|
end
|
|
@@ -115,7 +140,7 @@ module Mammoth
|
|
|
115
140
|
end
|
|
116
141
|
|
|
117
142
|
def build_source
|
|
118
|
-
Sources::Postgres.new(config, checkpoint_store: checkpoint_store)
|
|
143
|
+
Sources::Postgres.new(config, checkpoint_store: checkpoint_store, logger: logger)
|
|
119
144
|
end
|
|
120
145
|
|
|
121
146
|
def build_progress_coordinator
|
|
@@ -125,7 +150,8 @@ module Mammoth
|
|
|
125
150
|
slot_name: config.dig("replication", "slot"),
|
|
126
151
|
publication_name: Array(config.dig("replication", "publications")).join(","),
|
|
127
152
|
acknowledger: source_acknowledger,
|
|
128
|
-
position_resolver: source_position_resolver
|
|
153
|
+
position_resolver: source_position_resolver,
|
|
154
|
+
logger: logger
|
|
129
155
|
)
|
|
130
156
|
end
|
|
131
157
|
|
|
@@ -147,7 +173,8 @@ module Mammoth
|
|
|
147
173
|
dead_letter_store: state_adapter.dead_letter_store,
|
|
148
174
|
delivered_envelope_store: state_adapter.delivered_envelope_store,
|
|
149
175
|
sleeper: sleeper,
|
|
150
|
-
delivery_policy: delivery_policy
|
|
176
|
+
delivery_policy: delivery_policy,
|
|
177
|
+
logger: logger
|
|
151
178
|
)
|
|
152
179
|
end
|
|
153
180
|
|
data/lib/mammoth/cli.rb
CHANGED
|
@@ -109,7 +109,7 @@ module Mammoth
|
|
|
109
109
|
def observability
|
|
110
110
|
config = load_config
|
|
111
111
|
server = ObservabilityServer.new(config)
|
|
112
|
-
|
|
112
|
+
Logging.build(config).info("observability_started", host: server.host, port: server.port)
|
|
113
113
|
server.start
|
|
114
114
|
0
|
|
115
115
|
end
|
|
@@ -36,7 +36,7 @@ module Mammoth
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
attr_reader :checkpoint_store, :source_name, :slot_name, :publication_name
|
|
39
|
+
attr_reader :checkpoint_store, :source_name, :slot_name, :publication_name, :logger
|
|
40
40
|
|
|
41
41
|
# @param checkpoint_store [Mammoth::CheckpointStore] durable checkpoint persistence
|
|
42
42
|
# @param source_name [String] logical source name
|
|
@@ -45,13 +45,14 @@ module Mammoth
|
|
|
45
45
|
# @param acknowledger [#call, nil] upstream durable-progress acknowledgement
|
|
46
46
|
# @param position_resolver [#call, nil] source-owned durable position resolver
|
|
47
47
|
def initialize(checkpoint_store:, source_name:, slot_name:, publication_name:, acknowledger: nil,
|
|
48
|
-
position_resolver: nil)
|
|
48
|
+
position_resolver: nil, logger: Logging::NullLogger::INSTANCE)
|
|
49
49
|
@checkpoint_store = checkpoint_store
|
|
50
50
|
@source_name = source_name
|
|
51
51
|
@slot_name = slot_name
|
|
52
52
|
@publication_name = publication_name
|
|
53
53
|
@acknowledger = acknowledger
|
|
54
54
|
@position_resolver = position_resolver
|
|
55
|
+
@logger = logger
|
|
55
56
|
# rubocop:disable Layout/LeadingCommentSpace -- Steep inline type syntax requires `#:`.
|
|
56
57
|
@groups = [] #: Array[Group]
|
|
57
58
|
@entries_by_work = {} #: Hash[untyped, Array[Entry]]
|
|
@@ -133,6 +134,8 @@ module Mammoth
|
|
|
133
134
|
last_lsn: position
|
|
134
135
|
)
|
|
135
136
|
@acknowledger&.call(position)
|
|
137
|
+
logger.info("checkpoint_advanced", mammoth_name: source_name, slot: slot_name, publication: publication_name,
|
|
138
|
+
source_position: position)
|
|
136
139
|
end
|
|
137
140
|
|
|
138
141
|
def remove_group(group)
|
|
@@ -7,12 +7,12 @@ module Mammoth
|
|
|
7
7
|
# the delivery contract small: attempt webhook delivery, record idempotent
|
|
8
8
|
# success, and persist exhausted failures to the dead letter queue. Contiguous
|
|
9
9
|
# checkpointing is owned by DeliveryProgressCoordinator.
|
|
10
|
-
class DeliveryWorker
|
|
10
|
+
class DeliveryWorker # rubocop:disable Metrics/ClassLength
|
|
11
11
|
# Default source name used when an event does not provide one.
|
|
12
12
|
DEFAULT_SOURCE = "postgresql"
|
|
13
13
|
|
|
14
14
|
attr_reader :sink, :checkpoint_store, :dead_letter_store, :delivered_envelope_store, :retry_schedule, :max_attempts,
|
|
15
|
-
:sleeper, :source_name, :slot_name, :publication_name, :route_filter, :payload_policy, :enabled
|
|
15
|
+
:sleeper, :source_name, :slot_name, :publication_name, :route_filter, :payload_policy, :enabled, :logger
|
|
16
16
|
|
|
17
17
|
# @param sink [#deliver] destination sink
|
|
18
18
|
# @param checkpoint_store [Mammoth::CheckpointStore] retained dependency; shared progress owns writes
|
|
@@ -29,7 +29,7 @@ module Mammoth
|
|
|
29
29
|
# @param enabled [Boolean] whether this destination accepts new deliveries
|
|
30
30
|
def initialize(sink:, checkpoint_store:, dead_letter_store:, delivered_envelope_store:, source_name:, slot_name:,
|
|
31
31
|
publication_name:, max_attempts:, retry_schedule:, sleeper: Kernel.method(:sleep),
|
|
32
|
-
route_filter: nil, payload_policy: nil, enabled: true)
|
|
32
|
+
route_filter: nil, payload_policy: nil, enabled: true, logger: Logging::NullLogger::INSTANCE)
|
|
33
33
|
@sink = sink
|
|
34
34
|
@checkpoint_store = checkpoint_store
|
|
35
35
|
@dead_letter_store = dead_letter_store
|
|
@@ -43,6 +43,7 @@ module Mammoth
|
|
|
43
43
|
@route_filter = route_filter || RouteFilter.new
|
|
44
44
|
@payload_policy = payload_policy || PayloadPolicy.new
|
|
45
45
|
@enabled = enabled
|
|
46
|
+
@logger = logger
|
|
46
47
|
return unless @payload_policy.active? && !sink.respond_to?(:deliver_payload)
|
|
47
48
|
|
|
48
49
|
raise ConfigurationError, "destination #{destination_name} does not accept prepared payloads"
|
|
@@ -58,7 +59,7 @@ module Mammoth
|
|
|
58
59
|
# @param sleeper [#call] sleep strategy
|
|
59
60
|
# @return [Mammoth::DeliveryWorker]
|
|
60
61
|
def self.from_config(config, sink:, checkpoint_store:, dead_letter_store:, delivered_envelope_store:,
|
|
61
|
-
sleeper: Kernel.method(:sleep), delivery_policy: {})
|
|
62
|
+
sleeper: Kernel.method(:sleep), delivery_policy: {}, logger: Logging::NullLogger::INSTANCE)
|
|
62
63
|
new(
|
|
63
64
|
sink: sink,
|
|
64
65
|
checkpoint_store: checkpoint_store,
|
|
@@ -72,7 +73,8 @@ module Mammoth
|
|
|
72
73
|
sleeper: sleeper,
|
|
73
74
|
route_filter: delivery_policy.fetch("route_filter", RouteFilter.new),
|
|
74
75
|
payload_policy: delivery_policy.fetch("payload_policy", PayloadPolicy.new),
|
|
75
|
-
enabled: delivery_policy.fetch("enabled", true)
|
|
76
|
+
enabled: delivery_policy.fetch("enabled", true),
|
|
77
|
+
logger: logger
|
|
76
78
|
)
|
|
77
79
|
end
|
|
78
80
|
|
|
@@ -109,7 +111,7 @@ module Mammoth
|
|
|
109
111
|
deliver_prepared(prepared, work: work, delivery_method: delivery_method)
|
|
110
112
|
end
|
|
111
113
|
|
|
112
|
-
# rubocop:disable Metrics/MethodLength
|
|
114
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
113
115
|
def deliver_prepared(prepared, work:, delivery_method:)
|
|
114
116
|
attempts = 0
|
|
115
117
|
canonical_payload = prepared.canonical_payload
|
|
@@ -118,16 +120,21 @@ module Mammoth
|
|
|
118
120
|
idempotency_key = idempotency_key_for(payload: canonical_payload, delivery_unit: delivery_unit)
|
|
119
121
|
|
|
120
122
|
skip_result = skip_result_for(canonical_payload, idempotency_key: idempotency_key)
|
|
121
|
-
|
|
123
|
+
if skip_result
|
|
124
|
+
log_outcome("delivery_skipped", skip_result, delivery_unit:)
|
|
125
|
+
return skip_result
|
|
126
|
+
end
|
|
122
127
|
|
|
123
128
|
if delivered_envelope_store.delivered?(idempotency_key)
|
|
124
|
-
|
|
129
|
+
result = {
|
|
125
130
|
status: "skipped",
|
|
126
131
|
duplicate: true,
|
|
127
132
|
idempotency_key: idempotency_key,
|
|
128
133
|
attempts: attempts,
|
|
129
134
|
destination: destination_name
|
|
130
135
|
}
|
|
136
|
+
log_outcome("delivery_skipped", result, delivery_unit:)
|
|
137
|
+
return result
|
|
131
138
|
end
|
|
132
139
|
|
|
133
140
|
begin
|
|
@@ -142,15 +149,19 @@ module Mammoth
|
|
|
142
149
|
transaction_id: payload["transaction_id"],
|
|
143
150
|
source_position: payload["source_position"]
|
|
144
151
|
)
|
|
145
|
-
result.merge(attempts: attempts, idempotency_key: idempotency_key)
|
|
152
|
+
result = result.merge(attempts: attempts, idempotency_key: idempotency_key)
|
|
153
|
+
log_outcome("delivery_succeeded", result, delivery_unit:)
|
|
154
|
+
result
|
|
146
155
|
rescue DeliveryError => e
|
|
147
156
|
return dead_letter(payload, e, attempts) if attempts >= max_attempts
|
|
148
157
|
|
|
149
|
-
wait_before_retry(attempts)
|
|
158
|
+
wait_seconds = wait_before_retry(attempts)
|
|
159
|
+
logger.warn("delivery_retry", destination: destination_name, delivery_unit:, attempt: attempts,
|
|
160
|
+
wait_seconds:, error_class: e.class.name)
|
|
150
161
|
retry
|
|
151
162
|
end
|
|
152
163
|
end
|
|
153
|
-
# rubocop:enable Metrics/MethodLength
|
|
164
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
154
165
|
|
|
155
166
|
def idempotency_key_for(payload:, delivery_unit:)
|
|
156
167
|
[
|
|
@@ -191,6 +202,7 @@ module Mammoth
|
|
|
191
202
|
def wait_before_retry(attempts)
|
|
192
203
|
wait_seconds = retry_schedule.fetch(attempts - 1, retry_schedule.last)
|
|
193
204
|
sleeper.call(wait_seconds)
|
|
205
|
+
wait_seconds
|
|
194
206
|
end
|
|
195
207
|
|
|
196
208
|
def deliver_to_sink(prepared, work:, delivery_method:)
|
|
@@ -207,9 +219,18 @@ module Mammoth
|
|
|
207
219
|
error: error,
|
|
208
220
|
retry_count: attempts
|
|
209
221
|
)
|
|
222
|
+
logger.error(
|
|
223
|
+
"delivery_dead_lettered",
|
|
224
|
+
destination: destination_name, attempts:, dead_letter_id: id, error_class: error.class.name
|
|
225
|
+
)
|
|
210
226
|
{ status: "dead_lettered", dead_letter_id: id, attempts: attempts }
|
|
211
227
|
end
|
|
212
228
|
|
|
229
|
+
def log_outcome(event, result, delivery_unit:)
|
|
230
|
+
logger.info(event, destination: destination_name, delivery_unit:, status: result[:status],
|
|
231
|
+
attempts: result[:attempts], reason: result[:reason], duplicate: result[:duplicate])
|
|
232
|
+
end
|
|
233
|
+
|
|
213
234
|
def transaction_payload?(payload)
|
|
214
235
|
payload["type"] == TransactionEnvelopeSerializer::PAYLOAD_TYPE
|
|
215
236
|
end
|
|
@@ -7,6 +7,7 @@ module Mammoth
|
|
|
7
7
|
# future control agents stable observation points without changing the data
|
|
8
8
|
# plane or introducing remote behavior into OSS.
|
|
9
9
|
class LifecycleHooks
|
|
10
|
+
# Supported lifecycle callback names.
|
|
10
11
|
EVENTS = %i[
|
|
11
12
|
before_start
|
|
12
13
|
after_start
|
|
@@ -15,6 +16,8 @@ module Mammoth
|
|
|
15
16
|
before_replay
|
|
16
17
|
after_replay
|
|
17
18
|
].freeze
|
|
19
|
+
|
|
20
|
+
# Frozen empty callback list used for events without registrations.
|
|
18
21
|
EMPTY_CALLBACKS = [nil].compact.freeze
|
|
19
22
|
|
|
20
23
|
attr_reader :callbacks
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Mammoth
|
|
7
|
+
# Container-friendly structured application logging.
|
|
8
|
+
module Logging
|
|
9
|
+
# Supported severity names mapped to their filtering priority.
|
|
10
|
+
LEVELS = { "debug" => 0, "info" => 1, "warn" => 2, "error" => 3 }.freeze
|
|
11
|
+
|
|
12
|
+
# Logger that emits one JSON object per line.
|
|
13
|
+
class Logger
|
|
14
|
+
attr_reader :level, :output, :clock
|
|
15
|
+
|
|
16
|
+
def initialize(level:, output: $stdout, clock: -> { Time.now.utc })
|
|
17
|
+
@level = level.to_s
|
|
18
|
+
@output = output
|
|
19
|
+
@clock = clock
|
|
20
|
+
raise ConfigurationError, "unsupported logging level: #{@level}" unless LEVELS.key?(@level)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Emit a debug event when debug logging is enabled.
|
|
24
|
+
#
|
|
25
|
+
# @param event [String, Symbol] stable event name
|
|
26
|
+
# @param context [Hash] structured event fields
|
|
27
|
+
# @return [Boolean] whether the event was emitted
|
|
28
|
+
def debug(event, **context) = write("debug", event, context)
|
|
29
|
+
|
|
30
|
+
# Emit an informational event when info logging is enabled.
|
|
31
|
+
#
|
|
32
|
+
# @param event [String, Symbol] stable event name
|
|
33
|
+
# @param context [Hash] structured event fields
|
|
34
|
+
# @return [Boolean] whether the event was emitted
|
|
35
|
+
def info(event, **context) = write("info", event, context)
|
|
36
|
+
|
|
37
|
+
# Emit a warning event when warn logging is enabled.
|
|
38
|
+
#
|
|
39
|
+
# @param event [String, Symbol] stable event name
|
|
40
|
+
# @param context [Hash] structured event fields
|
|
41
|
+
# @return [Boolean] whether the event was emitted
|
|
42
|
+
def warn(event, **context) = write("warn", event, context)
|
|
43
|
+
|
|
44
|
+
# Emit an error event.
|
|
45
|
+
#
|
|
46
|
+
# @param event [String, Symbol] stable event name
|
|
47
|
+
# @param context [Hash] structured event fields
|
|
48
|
+
# @return [Boolean] whether the event was emitted
|
|
49
|
+
def error(event, **context) = write("error", event, context)
|
|
50
|
+
|
|
51
|
+
def enabled?(severity)
|
|
52
|
+
LEVELS.fetch(severity.to_s) >= LEVELS.fetch(level)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def write(severity, event, context)
|
|
58
|
+
return false unless enabled?(severity)
|
|
59
|
+
|
|
60
|
+
output.puts JSON.generate(
|
|
61
|
+
{ timestamp: clock.call.utc.iso8601(6), severity:, service: "mammoth", event: }.merge(context.compact)
|
|
62
|
+
)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# No-op logger used at injectable library boundaries.
|
|
68
|
+
class NullLogger
|
|
69
|
+
# Shared immutable no-op logger.
|
|
70
|
+
INSTANCE = new
|
|
71
|
+
|
|
72
|
+
# Suppress a debug event.
|
|
73
|
+
#
|
|
74
|
+
# @return [false]
|
|
75
|
+
def debug(_event, **_context) = false
|
|
76
|
+
|
|
77
|
+
# Suppress an informational event.
|
|
78
|
+
#
|
|
79
|
+
# @return [false]
|
|
80
|
+
def info(_event, **_context) = false
|
|
81
|
+
|
|
82
|
+
# Suppress a warning event.
|
|
83
|
+
#
|
|
84
|
+
# @return [false]
|
|
85
|
+
def warn(_event, **_context) = false
|
|
86
|
+
|
|
87
|
+
# Suppress an error event.
|
|
88
|
+
#
|
|
89
|
+
# @return [false]
|
|
90
|
+
def error(_event, **_context) = false
|
|
91
|
+
|
|
92
|
+
def enabled?(_severity) = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
# Build the configured structured logger.
|
|
98
|
+
#
|
|
99
|
+
# @param config [Mammoth::Configuration, Hash] loaded Mammoth configuration
|
|
100
|
+
# @param output [IO] destination for newline-delimited JSON
|
|
101
|
+
# @param clock [#call] UTC-compatible time source
|
|
102
|
+
# @return [Mammoth::Logging::Logger]
|
|
103
|
+
def build(config, output: $stdout, clock: -> { Time.now.utc })
|
|
104
|
+
Logger.new(level: config.dig("logging", "level") || "info", output:, clock:)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -11,7 +11,7 @@ module Mammoth
|
|
|
11
11
|
# the same configured operational-state backend.
|
|
12
12
|
class ObservabilityServer
|
|
13
13
|
# Default bind host for the observability server.
|
|
14
|
-
DEFAULT_HOST = "
|
|
14
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
15
15
|
# Default TCP port for the observability server.
|
|
16
16
|
DEFAULT_PORT = 9393
|
|
17
17
|
|
|
@@ -29,7 +29,7 @@ module Mammoth
|
|
|
29
29
|
@port = port || config.dig("observability", "port") || DEFAULT_PORT
|
|
30
30
|
@state_adapter = state_adapter || OperationalState::Registry.build_configured(config)
|
|
31
31
|
@slot_health_provider = slot_health_provider || Sources::Postgres.new(config)
|
|
32
|
-
@logger = logger || WEBrick::Log.new($
|
|
32
|
+
@logger = logger || WEBrick::Log.new($stdout, webrick_log_level)
|
|
33
33
|
@server = build_server
|
|
34
34
|
mount_endpoints
|
|
35
35
|
end
|
|
@@ -50,6 +50,15 @@ module Mammoth
|
|
|
50
50
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
|
+
def webrick_log_level
|
|
54
|
+
{
|
|
55
|
+
"debug" => WEBrick::Log::DEBUG,
|
|
56
|
+
"info" => WEBrick::Log::INFO,
|
|
57
|
+
"warn" => WEBrick::Log::WARN,
|
|
58
|
+
"error" => WEBrick::Log::ERROR
|
|
59
|
+
}.fetch(config.dig("logging", "level") || "info")
|
|
60
|
+
end
|
|
61
|
+
|
|
53
62
|
def build_server
|
|
54
63
|
WEBrick::HTTPServer.new(
|
|
55
64
|
BindAddress: host,
|
|
@@ -10,8 +10,13 @@ module Mammoth
|
|
|
10
10
|
# items. Column rules scrub every canonical row-value representation: data,
|
|
11
11
|
# identity, and old/new values in changes.
|
|
12
12
|
class PayloadPolicy
|
|
13
|
+
# Default replacement used by mask rules.
|
|
13
14
|
DEFAULT_MASK = "[REDACTED]"
|
|
15
|
+
|
|
16
|
+
# Payload metadata key containing the applied policy fingerprint.
|
|
14
17
|
POLICY_METADATA_KEY = "mammoth_payload_policy"
|
|
18
|
+
|
|
19
|
+
# Transformation actions accepted by payload-policy rules.
|
|
15
20
|
SUPPORTED_ACTIONS = %w[remove mask].freeze
|
|
16
21
|
|
|
17
22
|
attr_reader :config, :fingerprint
|
|
@@ -29,6 +29,8 @@ module Mammoth
|
|
|
29
29
|
attr_reader :checkpoint_store
|
|
30
30
|
# @return [#inspect, nil] injected PostgreSQL publication inspector
|
|
31
31
|
attr_reader :publication_inspector
|
|
32
|
+
# @return [Mammoth::Logging::Logger, Mammoth::Logging::NullLogger] operational logger
|
|
33
|
+
attr_reader :logger
|
|
32
34
|
|
|
33
35
|
# Build a PostgreSQL CDC source.
|
|
34
36
|
#
|
|
@@ -40,7 +42,7 @@ module Mammoth
|
|
|
40
42
|
# @param checkpoint_store [Mammoth::CheckpointStore, nil] persisted checkpoints for restart resume
|
|
41
43
|
# @param publication_inspector [#inspect, nil] injected publication metadata inspector
|
|
42
44
|
def initialize(config, runner: nil, parser: nil, decoder: nil, adapter: nil, checkpoint_store: nil,
|
|
43
|
-
publication_inspector: nil)
|
|
45
|
+
publication_inspector: nil, logger: Logging::NullLogger::INSTANCE)
|
|
44
46
|
@config = config
|
|
45
47
|
@runner = runner
|
|
46
48
|
@parser = parser
|
|
@@ -48,6 +50,7 @@ module Mammoth
|
|
|
48
50
|
@adapter = adapter
|
|
49
51
|
@checkpoint_store = checkpoint_store
|
|
50
52
|
@publication_inspector = publication_inspector
|
|
53
|
+
@logger = logger
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
# Stream CDC::Core work from PostgreSQL logical replication.
|
|
@@ -61,9 +64,12 @@ module Mammoth
|
|
|
61
64
|
# @yieldparam work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
|
|
62
65
|
# @return [Enumerator, nil]
|
|
63
66
|
# @raise [Mammoth::ReplicationError] when the source cannot stream CDC work
|
|
67
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
64
68
|
def each(&block)
|
|
65
69
|
return enum_for(:each) unless block_given?
|
|
66
70
|
|
|
71
|
+
logger.info("replication_starting", slot: config.dig("replication", "slot"),
|
|
72
|
+
publications: Array(config.dig("replication", "publications")))
|
|
67
73
|
preflight_slot!
|
|
68
74
|
preflight_replica_identity!
|
|
69
75
|
normalizer = effective_adapter
|
|
@@ -72,22 +78,29 @@ module Mammoth
|
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
normalizer.each_normalized(decoded_stream) do |work|
|
|
75
|
-
|
|
81
|
+
core_work = validate_core_work!(work)
|
|
82
|
+
logger.debug("replication_work_normalized", work_type: core_work.class.name,
|
|
83
|
+
source_position: @latest_progress_position)
|
|
84
|
+
yield_with_progress_position(core_work, &block)
|
|
76
85
|
end
|
|
86
|
+
logger.info("replication_stopped", slot: config.dig("replication", "slot"))
|
|
77
87
|
nil
|
|
78
88
|
rescue StandardError => e
|
|
89
|
+
logger.error("replication_failed", slot: config.dig("replication", "slot"), error_class: e.class.name)
|
|
79
90
|
raise e if e.is_a?(ReplicationError)
|
|
80
91
|
|
|
81
92
|
raise ReplicationError, "PostgreSQL CDC source failed: #{e.message}"
|
|
82
93
|
end
|
|
94
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
83
95
|
|
|
84
96
|
# Acknowledge a durably handled PostgreSQL WAL position.
|
|
85
97
|
#
|
|
86
98
|
# @param lsn [String, Integer] pgoutput-client compatible WAL position
|
|
87
99
|
# @return [Integer] normalized acknowledged WAL position
|
|
88
100
|
def acknowledge(lsn)
|
|
89
|
-
effective_runner.ack(lsn)
|
|
101
|
+
effective_runner.ack(lsn).tap { logger.debug("wal_acknowledged", slot: config.dig("replication", "slot"), lsn:) }
|
|
90
102
|
rescue StandardError => e
|
|
103
|
+
logger.error("wal_acknowledgement_failed", slot: config.dig("replication", "slot"), error_class: e.class.name)
|
|
91
104
|
raise ReplicationError, "PostgreSQL WAL acknowledgement failed: #{e.message}"
|
|
92
105
|
end
|
|
93
106
|
|
|
@@ -70,6 +70,7 @@ module Mammoth
|
|
|
70
70
|
|
|
71
71
|
# Read-only PostgreSQL publication and replica-identity catalog inspector.
|
|
72
72
|
class PostgresPublicationInspector
|
|
73
|
+
# Catalog query used to inspect publication actions and replica identity.
|
|
73
74
|
QUERY = <<~SQL
|
|
74
75
|
SELECT
|
|
75
76
|
relation.oid AS relation_id,
|
data/lib/mammoth/version.rb
CHANGED
data/lib/mammoth.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "mammoth/version"
|
|
|
6
6
|
require_relative "mammoth/errors"
|
|
7
7
|
require_relative "mammoth/registry"
|
|
8
8
|
require_relative "mammoth/configuration"
|
|
9
|
+
require_relative "mammoth/logging"
|
|
9
10
|
require_relative "mammoth/lifecycle_hooks"
|
|
10
11
|
require_relative "mammoth/node_identity"
|
|
11
12
|
require_relative "mammoth/capabilities"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mammoth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -37,6 +37,26 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: json
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 2.19.2
|
|
47
|
+
- - "<"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '3.0'
|
|
50
|
+
type: :runtime
|
|
51
|
+
prerelease: false
|
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: 2.19.2
|
|
57
|
+
- - "<"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.0'
|
|
40
60
|
- !ruby/object:Gem::Dependency
|
|
41
61
|
name: json-schema
|
|
42
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,14 +182,15 @@ dependencies:
|
|
|
162
182
|
- !ruby/object:Gem::Version
|
|
163
183
|
version: '1.9'
|
|
164
184
|
description: |
|
|
165
|
-
Mammoth is
|
|
185
|
+
Mammoth Data Plane is the open-source PostgreSQL change-event delivery
|
|
186
|
+
runtime in the Mammoth ecosystem.
|
|
166
187
|
|
|
167
188
|
It realizes the CDC Ecosystem pgoutput and cdc-core libraries for PostgreSQL,
|
|
168
189
|
then delivers normalized changes to webhook endpoints with durable
|
|
169
190
|
checkpointing, retry state, dead letters, and operational visibility.
|
|
170
191
|
|
|
171
|
-
Mammoth is application-first: it can be installed as a Ruby gem,
|
|
172
|
-
into a container image, or deployed into Kubernetes with Helm.
|
|
192
|
+
Mammoth Data Plane is application-first: it can be installed as a Ruby gem,
|
|
193
|
+
packaged into a container image, or deployed into Kubernetes with Helm.
|
|
173
194
|
email:
|
|
174
195
|
- kenneth.c.demanawa@gmail.com
|
|
175
196
|
executables:
|
|
@@ -211,6 +232,7 @@ files:
|
|
|
211
232
|
- lib/mammoth/event_serializer.rb
|
|
212
233
|
- lib/mammoth/fanout_delivery_worker.rb
|
|
213
234
|
- lib/mammoth/lifecycle_hooks.rb
|
|
235
|
+
- lib/mammoth/logging.rb
|
|
214
236
|
- lib/mammoth/metrics_observer.rb
|
|
215
237
|
- lib/mammoth/node_identity.rb
|
|
216
238
|
- lib/mammoth/observability_metrics.rb
|
|
@@ -265,6 +287,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
265
287
|
requirements: []
|
|
266
288
|
rubygems_version: 4.0.10
|
|
267
289
|
specification_version: 4
|
|
268
|
-
summary:
|
|
269
|
-
dead letters, and operational state.
|
|
290
|
+
summary: Mammoth Data Plane for reliable PostgreSQL change-event delivery.
|
|
270
291
|
test_files: []
|