mammoth 0.3.0 → 0.4.0
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 +7 -1
- data/README.md +2 -0
- data/config/mammoth.example.yml +7 -0
- data/config/mammoth.schema.json +18 -0
- data/lib/mammoth/cli.rb +11 -1
- data/lib/mammoth/observability_server.rb +85 -0
- data/lib/mammoth/observability_snapshot.rb +129 -0
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fdbb7afb90f584e11024c75d7300f54978f5e5b1a3628597bb460f96a306c22
|
|
4
|
+
data.tar.gz: 9ced2250d7e2f95558a8bcce51809aa4d670d49953d8d970e168a0c48cf90215
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd6b5b3246cf70adb9beb0cc9243e6a35788d20f3442ab406bca66a4cb08bd4a7d80639386c4e42291b518190a0198f960ed9e16cbd165f335a7f3fee10317b7
|
|
7
|
+
data.tar.gz: 87aa4ee77c38aa69d1a505bf7eb2b838de86504316840dfc26b4e875870333f64da906c85aa180a205e5e3bff4f439e36d2f82ffa79d650117d9f5ccc4c29b69
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -53,6 +53,7 @@ https://kanutocd.github.io/mammoth/Mammoth.html
|
|
|
53
53
|
- Docker image support
|
|
54
54
|
- public Helm chart support
|
|
55
55
|
- unit and e2e test tasks
|
|
56
|
+
- Health and metrics endpoints
|
|
56
57
|
|
|
57
58
|
## Boundary
|
|
58
59
|
|
|
@@ -84,6 +85,7 @@ bundle exec ./exe/mammoth validate config/mammoth.example.yml
|
|
|
84
85
|
bundle exec ./exe/mammoth bootstrap config/mammoth.example.yml
|
|
85
86
|
bundle exec ./exe/mammoth status config/mammoth.example.yml
|
|
86
87
|
bundle exec ./exe/mammoth start config/mammoth.example.yml
|
|
88
|
+
bundle exec ./exe/mammoth observability config/mammoth.example.yml
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
Deliver a single normalized event JSON file through Mammoth's delivery path:
|
data/config/mammoth.example.yml
CHANGED
|
@@ -274,6 +274,13 @@ sqlite:
|
|
|
274
274
|
# In Kubernetes, back this path with a PersistentVolumeClaim.
|
|
275
275
|
path: data/mammoth.db
|
|
276
276
|
|
|
277
|
+
observability:
|
|
278
|
+
# Bind host for the optional health, readiness, and metrics server.
|
|
279
|
+
host: 0.0.0.0
|
|
280
|
+
|
|
281
|
+
# HTTP port for /healthz, /readyz, and /metrics.
|
|
282
|
+
port: 9393
|
|
283
|
+
|
|
277
284
|
logging:
|
|
278
285
|
# Log verbosity.
|
|
279
286
|
#
|
data/config/mammoth.schema.json
CHANGED
|
@@ -319,6 +319,24 @@
|
|
|
319
319
|
}
|
|
320
320
|
},
|
|
321
321
|
"description": "Downstream delivery runtime settings. Runtime concurrency affects webhook/destination delivery only, not PostgreSQL replication stream count."
|
|
322
|
+
},
|
|
323
|
+
"observability": {
|
|
324
|
+
"type": "object",
|
|
325
|
+
"additionalProperties": false,
|
|
326
|
+
"properties": {
|
|
327
|
+
"host": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"minLength": 1,
|
|
330
|
+
"description": "Bind host for the optional Mammoth observability HTTP server exposing /healthz, /readyz, and /metrics."
|
|
331
|
+
},
|
|
332
|
+
"port": {
|
|
333
|
+
"type": "integer",
|
|
334
|
+
"minimum": 1,
|
|
335
|
+
"maximum": 65535,
|
|
336
|
+
"description": "TCP port for the optional Mammoth observability HTTP server."
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"description": "Optional health, readiness, and metrics endpoint settings."
|
|
322
340
|
}
|
|
323
341
|
},
|
|
324
342
|
"description": "Mammoth relay configuration. Mammoth consumes PostgreSQL logical replication changes and delivers them to downstream destinations while preserving checkpoint, retry, and dead-letter semantics."
|
data/lib/mammoth/cli.rb
CHANGED
|
@@ -26,7 +26,8 @@ module Mammoth
|
|
|
26
26
|
" mammoth deliver-sample CONFIG EVENT_JSON",
|
|
27
27
|
" mammoth dead-letters list CONFIG [--status STATUS] [--limit N]",
|
|
28
28
|
" mammoth dead-letters show CONFIG ID",
|
|
29
|
-
" mammoth dead-letters replay CONFIG [ID ...]"
|
|
29
|
+
" mammoth dead-letters replay CONFIG [ID ...]",
|
|
30
|
+
" mammoth observability CONFIG"
|
|
30
31
|
].join("\n")
|
|
31
32
|
|
|
32
33
|
# Run the CLI.
|
|
@@ -56,6 +57,7 @@ module Mammoth
|
|
|
56
57
|
when "start" then start
|
|
57
58
|
when "deliver-sample" then deliver_sample
|
|
58
59
|
when "dead-letters" then dead_letters
|
|
60
|
+
when "observability" then observability
|
|
59
61
|
else
|
|
60
62
|
warn USAGE
|
|
61
63
|
1
|
|
@@ -130,6 +132,14 @@ module Mammoth
|
|
|
130
132
|
DeadLetterCommands.call(argv)
|
|
131
133
|
end
|
|
132
134
|
|
|
135
|
+
def observability
|
|
136
|
+
config = load_config
|
|
137
|
+
server = ObservabilityServer.new(config)
|
|
138
|
+
puts "Mammoth observability listening on #{server.host}:#{server.port}"
|
|
139
|
+
server.start
|
|
140
|
+
0
|
|
141
|
+
end
|
|
142
|
+
|
|
133
143
|
def load_config
|
|
134
144
|
raise ConfigurationError, "configuration path required\n#{USAGE}" unless config_path
|
|
135
145
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "webrick"
|
|
5
|
+
|
|
6
|
+
module Mammoth
|
|
7
|
+
# Small HTTP server exposing Mammoth health, readiness, and metrics endpoints.
|
|
8
|
+
#
|
|
9
|
+
# The server is intentionally independent from the replication loop. Operators
|
|
10
|
+
# may run it as a sidecar-like process or in a separate process that points at
|
|
11
|
+
# the same SQLite operational database.
|
|
12
|
+
class ObservabilityServer
|
|
13
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
14
|
+
DEFAULT_PORT = 9393
|
|
15
|
+
|
|
16
|
+
attr_reader :config, :host, :port, :sqlite_store, :logger, :server
|
|
17
|
+
|
|
18
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
19
|
+
# @param host [String, nil] bind host override
|
|
20
|
+
# @param port [Integer, nil] bind port override
|
|
21
|
+
# @param sqlite_store [Mammoth::SQLiteStore, nil] optional operational store
|
|
22
|
+
# @param logger [WEBrick::Log, nil] optional WEBrick logger
|
|
23
|
+
def initialize(config, host: nil, port: nil, sqlite_store: nil, logger: nil)
|
|
24
|
+
@config = config
|
|
25
|
+
@host = host || config.dig("observability", "host") || DEFAULT_HOST
|
|
26
|
+
@port = port || config.dig("observability", "port") || DEFAULT_PORT
|
|
27
|
+
@sqlite_store = sqlite_store
|
|
28
|
+
@logger = logger || WEBrick::Log.new($stderr, WEBrick::Log::WARN)
|
|
29
|
+
@server = build_server
|
|
30
|
+
mount_endpoints
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Start the blocking HTTP server.
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
36
|
+
def start
|
|
37
|
+
server.start
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Stop the HTTP server.
|
|
41
|
+
#
|
|
42
|
+
# @return [void]
|
|
43
|
+
def shutdown
|
|
44
|
+
server.shutdown
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_server
|
|
50
|
+
WEBrick::HTTPServer.new(
|
|
51
|
+
BindAddress: host,
|
|
52
|
+
Port: port,
|
|
53
|
+
Logger: logger,
|
|
54
|
+
AccessLog: []
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mount_endpoints
|
|
59
|
+
server.mount_proc("/healthz") { |_request, response| write_json(response, snapshot.health, status: 200) }
|
|
60
|
+
server.mount_proc("/readyz") { |_request, response| write_readiness(response) }
|
|
61
|
+
server.mount_proc("/metrics") { |_request, response| write_metrics(response) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write_readiness(response)
|
|
65
|
+
payload = snapshot.readiness
|
|
66
|
+
write_json(response, payload, status: payload.fetch(:status) == "ready" ? 200 : 503)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_metrics(response)
|
|
70
|
+
response.status = 200
|
|
71
|
+
response["Content-Type"] = "text/plain; version=0.0.4; charset=utf-8"
|
|
72
|
+
response.body = snapshot.prometheus
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_json(response, payload, status:)
|
|
76
|
+
response.status = status
|
|
77
|
+
response["Content-Type"] = "application/json"
|
|
78
|
+
response.body = JSON.generate(payload)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def snapshot
|
|
82
|
+
ObservabilitySnapshot.new(config, sqlite_store: sqlite_store)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Mammoth
|
|
6
|
+
# Builds health, readiness, and metrics snapshots from Mammoth's local
|
|
7
|
+
# operational state.
|
|
8
|
+
#
|
|
9
|
+
# ObservabilitySnapshot is intentionally read-only. It does not start the
|
|
10
|
+
# relay, mutate checkpoints, replay dead letters, or inspect PostgreSQL. The
|
|
11
|
+
# health and metrics endpoints use this object to expose Mammoth process and
|
|
12
|
+
# SQLite operational-state status in a predictable format.
|
|
13
|
+
class ObservabilitySnapshot
|
|
14
|
+
attr_reader :config, :sqlite_store, :clock
|
|
15
|
+
|
|
16
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
17
|
+
# @param sqlite_store [Mammoth::SQLiteStore, nil] optional operational store
|
|
18
|
+
# @param clock [#call] time source returning a Time-like object
|
|
19
|
+
def initialize(config, sqlite_store: nil, clock: -> { Time.now.utc })
|
|
20
|
+
@config = config
|
|
21
|
+
@sqlite_store = sqlite_store
|
|
22
|
+
@clock = clock
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build a liveness response.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash] health payload
|
|
28
|
+
def health
|
|
29
|
+
{
|
|
30
|
+
status: "ok",
|
|
31
|
+
service: "mammoth",
|
|
32
|
+
name: mammoth_name,
|
|
33
|
+
version: Mammoth::VERSION,
|
|
34
|
+
checked_at: checked_at
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build a readiness response.
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash] readiness payload
|
|
41
|
+
def readiness
|
|
42
|
+
store = operational_store
|
|
43
|
+
store.bootstrap!
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
status: "ready",
|
|
47
|
+
service: "mammoth",
|
|
48
|
+
name: mammoth_name,
|
|
49
|
+
sqlite: "ok",
|
|
50
|
+
tables: store.tables,
|
|
51
|
+
checked_at: checked_at
|
|
52
|
+
}
|
|
53
|
+
rescue Mammoth::Error, SQLite3::Exception => e
|
|
54
|
+
{
|
|
55
|
+
status: "unready",
|
|
56
|
+
service: "mammoth",
|
|
57
|
+
name: mammoth_name,
|
|
58
|
+
sqlite: "error",
|
|
59
|
+
error_class: e.class.name,
|
|
60
|
+
error_message: e.message,
|
|
61
|
+
checked_at: checked_at
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build a Prometheus text exposition document.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] Prometheus metrics text
|
|
68
|
+
def prometheus
|
|
69
|
+
store = operational_store.bootstrap!
|
|
70
|
+
checkpoint_store = CheckpointStore.new(store)
|
|
71
|
+
dead_letter_store = DeadLetterStore.new(store)
|
|
72
|
+
delivered_store = DeliveredEnvelopeStore.new(store)
|
|
73
|
+
|
|
74
|
+
lines = metric_headers + [
|
|
75
|
+
metric_line("mammoth_up", 1),
|
|
76
|
+
metric_line("mammoth_checkpoints_total", checkpoint_store.count),
|
|
77
|
+
metric_line("mammoth_dead_letters_total", dead_letter_store.count),
|
|
78
|
+
metric_line("mammoth_dead_letters_pending_total", dead_letter_store.count(status: "pending")),
|
|
79
|
+
metric_line("mammoth_dead_letters_resolved_total", dead_letter_store.count(status: "resolved")),
|
|
80
|
+
metric_line("mammoth_dead_letters_ignored_total", dead_letter_store.count(status: "ignored")),
|
|
81
|
+
metric_line("mammoth_delivered_envelopes_total", delivered_store.count)
|
|
82
|
+
]
|
|
83
|
+
"#{lines.join("\n")}\n"
|
|
84
|
+
rescue Mammoth::Error, SQLite3::Exception
|
|
85
|
+
"#{(metric_headers + [metric_line("mammoth_up", 0)]).join("\n")}\n"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def operational_store
|
|
91
|
+
sqlite_store || SQLiteStore.connect(config.dig("sqlite", "path"))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def mammoth_name
|
|
95
|
+
config.dig("mammoth", "name")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def checked_at
|
|
99
|
+
clock.call.utc.iso8601
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def metric_headers
|
|
103
|
+
[
|
|
104
|
+
"# HELP mammoth_up 1 when Mammoth operational state can be inspected, 0 otherwise.",
|
|
105
|
+
"# TYPE mammoth_up gauge",
|
|
106
|
+
"# HELP mammoth_checkpoints_total Number of checkpoint rows stored by Mammoth.",
|
|
107
|
+
"# TYPE mammoth_checkpoints_total gauge",
|
|
108
|
+
"# HELP mammoth_dead_letters_total Number of dead-letter rows stored by Mammoth.",
|
|
109
|
+
"# TYPE mammoth_dead_letters_total gauge",
|
|
110
|
+
"# HELP mammoth_dead_letters_pending_total Number of pending dead-letter rows.",
|
|
111
|
+
"# TYPE mammoth_dead_letters_pending_total gauge",
|
|
112
|
+
"# HELP mammoth_dead_letters_resolved_total Number of resolved dead-letter rows.",
|
|
113
|
+
"# TYPE mammoth_dead_letters_resolved_total gauge",
|
|
114
|
+
"# HELP mammoth_dead_letters_ignored_total Number of ignored dead-letter rows.",
|
|
115
|
+
"# TYPE mammoth_dead_letters_ignored_total gauge",
|
|
116
|
+
"# HELP mammoth_delivered_envelopes_total Number of delivered-envelope ledger rows.",
|
|
117
|
+
"# TYPE mammoth_delivered_envelopes_total gauge"
|
|
118
|
+
]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def metric_line(name, value)
|
|
122
|
+
%(#{name}{mammoth_name="#{escape_label(mammoth_name)}"} #{Integer(value)})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def escape_label(value)
|
|
126
|
+
value.to_s.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", "\\n")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/mammoth/version.rb
CHANGED
data/lib/mammoth.rb
CHANGED
|
@@ -4,6 +4,8 @@ require_relative "mammoth/version"
|
|
|
4
4
|
require_relative "mammoth/errors"
|
|
5
5
|
require_relative "mammoth/configuration"
|
|
6
6
|
require_relative "mammoth/status"
|
|
7
|
+
require_relative "mammoth/observability_snapshot"
|
|
8
|
+
require_relative "mammoth/observability_server"
|
|
7
9
|
require_relative "mammoth/sqlite_store"
|
|
8
10
|
require_relative "mammoth/checkpoint_store"
|
|
9
11
|
require_relative "mammoth/dead_letter_store"
|
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: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -157,6 +157,8 @@ files:
|
|
|
157
157
|
- lib/mammoth/delivery_worker.rb
|
|
158
158
|
- lib/mammoth/errors.rb
|
|
159
159
|
- lib/mammoth/event_serializer.rb
|
|
160
|
+
- lib/mammoth/observability_server.rb
|
|
161
|
+
- lib/mammoth/observability_snapshot.rb
|
|
160
162
|
- lib/mammoth/replication_consumer.rb
|
|
161
163
|
- lib/mammoth/sources/postgres.rb
|
|
162
164
|
- lib/mammoth/sql/__bootstrap__.sql
|