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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d7d570675d2b6731d783c6eba0f347ab00c1d7f9e524c20eef4908af129c41a
4
- data.tar.gz: f14c3223b20cd1ea8e7d795e1cd2fe4e512766a0a4ed275515e4d1174bb3d6f5
3
+ metadata.gz: 8fdbb7afb90f584e11024c75d7300f54978f5e5b1a3628597bb460f96a306c22
4
+ data.tar.gz: 9ced2250d7e2f95558a8bcce51809aa4d670d49953d8d970e168a0c48cf90215
5
5
  SHA512:
6
- metadata.gz: cf649aba9068bdc1c3b0e01c1254f8f16804e80ec88de725d2743e99be9b9010bd880590fde044c9d7dff3b0b99fac54c6c9776af4504fb566eeceb1e5b00fd9
7
- data.tar.gz: a7396f91f4fe84b3780f9b176931d728707b6e55ecab9b168b5eb3e87cb384c2066d9ed0b944dd14405d2e06a02e0e363a28348325578bb652aa4ba1df56caee
6
+ metadata.gz: cd6b5b3246cf70adb9beb0cc9243e6a35788d20f3442ab406bca66a4cb08bd4a7d80639386c4e42291b518190a0198f960ed9e16cbd165f335a7f3fee10317b7
7
+ data.tar.gz: 87aa4ee77c38aa69d1a505bf7eb2b838de86504316840dfc26b4e875870333f64da906c85aa180a205e5e3bff4f439e36d2f82ffa79d650117d9f5ccc4c29b69
data/CHANGELOG.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.3.0
5
+ ## 0.4.0
6
+
7
+ ### Added
8
+
9
+ - Added optional observability support with `/healthz`, `/readyz`, and `/metrics` endpoints, plus CLI startup, configuration, and documentation coverage.
10
+
11
+ ## 0.4.0
6
12
 
7
13
  ### Added
8
14
 
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:
@@ -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
  #
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
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.3.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