nats_pubsub 1.0.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 +7 -0
- data/exe/nats_pubsub +44 -0
- data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
- data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
- data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
- data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
- data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
- data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
- data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
- data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
- data/lib/nats_pubsub/active_record/publishable.rb +192 -0
- data/lib/nats_pubsub/cli.rb +105 -0
- data/lib/nats_pubsub/core/base_repository.rb +73 -0
- data/lib/nats_pubsub/core/config.rb +152 -0
- data/lib/nats_pubsub/core/config_presets.rb +139 -0
- data/lib/nats_pubsub/core/connection.rb +103 -0
- data/lib/nats_pubsub/core/constants.rb +190 -0
- data/lib/nats_pubsub/core/duration.rb +113 -0
- data/lib/nats_pubsub/core/error_action.rb +288 -0
- data/lib/nats_pubsub/core/event.rb +275 -0
- data/lib/nats_pubsub/core/health_check.rb +470 -0
- data/lib/nats_pubsub/core/logging.rb +72 -0
- data/lib/nats_pubsub/core/message_context.rb +193 -0
- data/lib/nats_pubsub/core/presets.rb +222 -0
- data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
- data/lib/nats_pubsub/core/structured_logger.rb +141 -0
- data/lib/nats_pubsub/core/subject.rb +185 -0
- data/lib/nats_pubsub/instrumentation.rb +327 -0
- data/lib/nats_pubsub/middleware/active_record.rb +18 -0
- data/lib/nats_pubsub/middleware/chain.rb +92 -0
- data/lib/nats_pubsub/middleware/logging.rb +48 -0
- data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
- data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
- data/lib/nats_pubsub/models/event_model.rb +73 -0
- data/lib/nats_pubsub/models/inbox_event.rb +109 -0
- data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
- data/lib/nats_pubsub/models/model_utils.rb +57 -0
- data/lib/nats_pubsub/models/outbox_event.rb +113 -0
- data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
- data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
- data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
- data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
- data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
- data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
- data/lib/nats_pubsub/publisher/publisher.rb +156 -0
- data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
- data/lib/nats_pubsub/railtie.rb +52 -0
- data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
- data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
- data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
- data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
- data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
- data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
- data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
- data/lib/nats_pubsub/subscribers/pool.rb +166 -0
- data/lib/nats_pubsub/subscribers/registry.rb +114 -0
- data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
- data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
- data/lib/nats_pubsub/subscribers/worker.rb +152 -0
- data/lib/nats_pubsub/tasks/install.rake +10 -0
- data/lib/nats_pubsub/testing/helpers.rb +199 -0
- data/lib/nats_pubsub/testing/matchers.rb +208 -0
- data/lib/nats_pubsub/testing/test_harness.rb +250 -0
- data/lib/nats_pubsub/testing.rb +157 -0
- data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
- data/lib/nats_pubsub/topology/stream.rb +102 -0
- data/lib/nats_pubsub/topology/stream_support.rb +170 -0
- data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
- data/lib/nats_pubsub/topology/topology.rb +24 -0
- data/lib/nats_pubsub/version.rb +8 -0
- data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
- data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
- data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
- data/lib/nats_pubsub/web/views/layout.erb +68 -0
- data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
- data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
- data/lib/nats_pubsub/web.rb +181 -0
- data/lib/nats_pubsub.rb +290 -0
- metadata +225 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NatsPubsub
|
|
4
|
+
module Core
|
|
5
|
+
# Health check for NatsPubsub components
|
|
6
|
+
#
|
|
7
|
+
# Provides comprehensive health checking for connection, JetStream, and streams.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic health check
|
|
10
|
+
# status = NatsPubsub::Core::HealthCheck.check
|
|
11
|
+
# puts "Status: #{status.status}"
|
|
12
|
+
# puts "Healthy: #{status.healthy?}"
|
|
13
|
+
#
|
|
14
|
+
# @example Quick health check
|
|
15
|
+
# status = NatsPubsub::Core::HealthCheck.quick_check
|
|
16
|
+
# puts "Status: #{status.status}"
|
|
17
|
+
#
|
|
18
|
+
class HealthCheck
|
|
19
|
+
# Health check result
|
|
20
|
+
#
|
|
21
|
+
# @!attribute [r] status
|
|
22
|
+
# @return [Symbol] Overall status (:healthy, :degraded, :unhealthy)
|
|
23
|
+
# @!attribute [r] components
|
|
24
|
+
# @return [Hash] Component health details
|
|
25
|
+
# @!attribute [r] timestamp
|
|
26
|
+
# @return [Time] Check timestamp
|
|
27
|
+
# @!attribute [r] duration
|
|
28
|
+
# @return [Float] Check duration in milliseconds
|
|
29
|
+
#
|
|
30
|
+
class Result
|
|
31
|
+
attr_reader :status, :components, :timestamp, :duration
|
|
32
|
+
|
|
33
|
+
def initialize(status:, components:, timestamp:, duration:)
|
|
34
|
+
@status = status
|
|
35
|
+
@components = components
|
|
36
|
+
@timestamp = timestamp
|
|
37
|
+
@duration = duration
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if system is healthy
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] True if healthy
|
|
44
|
+
def healthy?
|
|
45
|
+
status == :healthy
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if system is degraded
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] True if degraded
|
|
51
|
+
def degraded?
|
|
52
|
+
status == :degraded
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if system is unhealthy
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean] True if unhealthy
|
|
58
|
+
def unhealthy?
|
|
59
|
+
status == :unhealthy
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert to hash
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash] Hash representation
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
status: status,
|
|
68
|
+
healthy: healthy?,
|
|
69
|
+
components: components,
|
|
70
|
+
timestamp: timestamp.iso8601,
|
|
71
|
+
duration_ms: duration
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
alias to_hash to_h
|
|
76
|
+
|
|
77
|
+
# Convert to JSON
|
|
78
|
+
#
|
|
79
|
+
# @return [String] JSON string
|
|
80
|
+
def to_json(*_args)
|
|
81
|
+
require 'json'
|
|
82
|
+
to_h.to_json
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Component health details
|
|
87
|
+
#
|
|
88
|
+
# @!attribute [r] status
|
|
89
|
+
# @return [Symbol] Component status (:healthy, :unhealthy)
|
|
90
|
+
# @!attribute [r] message
|
|
91
|
+
# @return [String, nil] Status message
|
|
92
|
+
# @!attribute [r] details
|
|
93
|
+
# @return [Hash, nil] Additional details
|
|
94
|
+
#
|
|
95
|
+
class ComponentHealth
|
|
96
|
+
attr_reader :status, :message, :details
|
|
97
|
+
|
|
98
|
+
def initialize(status:, message: nil, details: nil)
|
|
99
|
+
@status = status
|
|
100
|
+
@message = message
|
|
101
|
+
@details = details
|
|
102
|
+
freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def healthy?
|
|
106
|
+
status == :healthy
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def unhealthy?
|
|
110
|
+
status == :unhealthy
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
{
|
|
115
|
+
status: status,
|
|
116
|
+
healthy: healthy?,
|
|
117
|
+
message: message,
|
|
118
|
+
details: details
|
|
119
|
+
}.compact
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
alias to_hash to_h
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class << self
|
|
126
|
+
# Perform comprehensive health check
|
|
127
|
+
#
|
|
128
|
+
# Checks:
|
|
129
|
+
# - NATS connection
|
|
130
|
+
# - JetStream availability
|
|
131
|
+
# - Stream configuration
|
|
132
|
+
# - Outbox pattern (if enabled)
|
|
133
|
+
# - Inbox pattern (if enabled)
|
|
134
|
+
# - Connection pool
|
|
135
|
+
#
|
|
136
|
+
# @return [Result] Health check result
|
|
137
|
+
def check
|
|
138
|
+
start_time = Time.now
|
|
139
|
+
components = {}
|
|
140
|
+
|
|
141
|
+
# Check NATS connection
|
|
142
|
+
components[:connection] = check_connection
|
|
143
|
+
|
|
144
|
+
# Check JetStream
|
|
145
|
+
components[:jetstream] = check_jetstream
|
|
146
|
+
|
|
147
|
+
# Check streams
|
|
148
|
+
components[:streams] = check_streams if components[:jetstream].healthy?
|
|
149
|
+
|
|
150
|
+
# Check outbox (if enabled)
|
|
151
|
+
outbox_health = check_outbox
|
|
152
|
+
components[:outbox] = outbox_health if outbox_health
|
|
153
|
+
|
|
154
|
+
# Check inbox (if enabled)
|
|
155
|
+
inbox_health = check_inbox
|
|
156
|
+
components[:inbox] = inbox_health if inbox_health
|
|
157
|
+
|
|
158
|
+
# Check connection pool
|
|
159
|
+
components[:connection_pool] = check_connection_pool
|
|
160
|
+
|
|
161
|
+
# Determine overall status
|
|
162
|
+
status = determine_status(components)
|
|
163
|
+
|
|
164
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
165
|
+
|
|
166
|
+
Result.new(
|
|
167
|
+
status: status,
|
|
168
|
+
components: components.transform_values(&:to_h),
|
|
169
|
+
timestamp: Time.now,
|
|
170
|
+
duration: duration
|
|
171
|
+
)
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
Result.new(
|
|
174
|
+
status: :unhealthy,
|
|
175
|
+
components: {
|
|
176
|
+
error: ComponentHealth.new(
|
|
177
|
+
status: :unhealthy,
|
|
178
|
+
message: "Health check failed: #{e.message}"
|
|
179
|
+
).to_h
|
|
180
|
+
},
|
|
181
|
+
timestamp: Time.now,
|
|
182
|
+
duration: ((Time.now - start_time) * 1000).round(2)
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Perform quick health check
|
|
187
|
+
#
|
|
188
|
+
# Only checks NATS connection for fast response.
|
|
189
|
+
#
|
|
190
|
+
# @return [Result] Health check result
|
|
191
|
+
def quick_check
|
|
192
|
+
start_time = Time.now
|
|
193
|
+
|
|
194
|
+
connection_health = check_connection
|
|
195
|
+
|
|
196
|
+
Result.new(
|
|
197
|
+
status: connection_health.healthy? ? :healthy : :unhealthy,
|
|
198
|
+
components: { connection: connection_health.to_h },
|
|
199
|
+
timestamp: Time.now,
|
|
200
|
+
duration: ((Time.now - start_time) * 1000).round(2)
|
|
201
|
+
)
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
Result.new(
|
|
204
|
+
status: :unhealthy,
|
|
205
|
+
components: {
|
|
206
|
+
error: ComponentHealth.new(
|
|
207
|
+
status: :unhealthy,
|
|
208
|
+
message: "Quick check failed: #{e.message}"
|
|
209
|
+
).to_h
|
|
210
|
+
},
|
|
211
|
+
timestamp: Time.now,
|
|
212
|
+
duration: ((Time.now - start_time) * 1000).round(2)
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Rack middleware for health check endpoint
|
|
217
|
+
#
|
|
218
|
+
# @example Sinatra
|
|
219
|
+
# get '/health' do
|
|
220
|
+
# status, headers, body = NatsPubsub::Core::HealthCheck.middleware.call(env)
|
|
221
|
+
# [status, headers, body]
|
|
222
|
+
# end
|
|
223
|
+
#
|
|
224
|
+
# @example Rails
|
|
225
|
+
# get '/health', to: proc { |env|
|
|
226
|
+
# NatsPubsub::Core::HealthCheck.middleware.call(env)
|
|
227
|
+
# }
|
|
228
|
+
#
|
|
229
|
+
# @return [Proc] Rack middleware
|
|
230
|
+
def middleware
|
|
231
|
+
lambda do |_env|
|
|
232
|
+
result = check
|
|
233
|
+
status_code = result.healthy? ? 200 : (result.degraded? ? 200 : 503)
|
|
234
|
+
|
|
235
|
+
[
|
|
236
|
+
status_code,
|
|
237
|
+
{ 'Content-Type' => 'application/json' },
|
|
238
|
+
[result.to_json]
|
|
239
|
+
]
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Rack middleware for quick health check endpoint
|
|
244
|
+
#
|
|
245
|
+
# @return [Proc] Rack middleware
|
|
246
|
+
def quick_middleware
|
|
247
|
+
lambda do |_env|
|
|
248
|
+
result = quick_check
|
|
249
|
+
status_code = result.healthy? ? 200 : 503
|
|
250
|
+
|
|
251
|
+
[
|
|
252
|
+
status_code,
|
|
253
|
+
{ 'Content-Type' => 'application/json' },
|
|
254
|
+
[result.to_json]
|
|
255
|
+
]
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
# Check NATS connection
|
|
262
|
+
#
|
|
263
|
+
# @return [ComponentHealth] Connection health
|
|
264
|
+
def check_connection
|
|
265
|
+
nc = Connection.connection
|
|
266
|
+
return ComponentHealth.new(status: :unhealthy, message: 'Not connected') if nc.nil?
|
|
267
|
+
|
|
268
|
+
if nc.closed?
|
|
269
|
+
ComponentHealth.new(status: :unhealthy, message: 'Connection closed')
|
|
270
|
+
else
|
|
271
|
+
ComponentHealth.new(
|
|
272
|
+
status: :healthy,
|
|
273
|
+
message: 'Connected',
|
|
274
|
+
details: {
|
|
275
|
+
server_info: nc.server_info.to_h
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
ComponentHealth.new(
|
|
281
|
+
status: :unhealthy,
|
|
282
|
+
message: "Connection check failed: #{e.message}"
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Check JetStream availability
|
|
287
|
+
#
|
|
288
|
+
# @return [ComponentHealth] JetStream health
|
|
289
|
+
def check_jetstream
|
|
290
|
+
nc = Connection.connection
|
|
291
|
+
return ComponentHealth.new(status: :unhealthy, message: 'Not connected') if nc.nil?
|
|
292
|
+
|
|
293
|
+
jsm = nc.jetstream_manager
|
|
294
|
+
account_info = jsm.account_info
|
|
295
|
+
|
|
296
|
+
ComponentHealth.new(
|
|
297
|
+
status: :healthy,
|
|
298
|
+
message: 'JetStream available',
|
|
299
|
+
details: {
|
|
300
|
+
streams: account_info.streams,
|
|
301
|
+
consumers: account_info.consumers,
|
|
302
|
+
memory: account_info.memory,
|
|
303
|
+
storage: account_info.storage
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
ComponentHealth.new(
|
|
308
|
+
status: :unhealthy,
|
|
309
|
+
message: "JetStream check failed: #{e.message}"
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Check configured streams
|
|
314
|
+
#
|
|
315
|
+
# @return [ComponentHealth] Streams health
|
|
316
|
+
def check_streams
|
|
317
|
+
nc = Connection.connection
|
|
318
|
+
jsm = nc.jetstream_manager
|
|
319
|
+
|
|
320
|
+
# Get configured stream names from topology
|
|
321
|
+
expected_streams = Topology.stream_configs.keys
|
|
322
|
+
|
|
323
|
+
existing_streams = jsm.streams.map(&:config).map(&:name)
|
|
324
|
+
|
|
325
|
+
missing_streams = expected_streams - existing_streams
|
|
326
|
+
|
|
327
|
+
if missing_streams.empty?
|
|
328
|
+
ComponentHealth.new(
|
|
329
|
+
status: :healthy,
|
|
330
|
+
message: 'All streams configured',
|
|
331
|
+
details: {
|
|
332
|
+
expected: expected_streams.size,
|
|
333
|
+
existing: existing_streams.size,
|
|
334
|
+
streams: existing_streams
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
else
|
|
338
|
+
ComponentHealth.new(
|
|
339
|
+
status: :unhealthy,
|
|
340
|
+
message: 'Missing streams',
|
|
341
|
+
details: {
|
|
342
|
+
expected: expected_streams.size,
|
|
343
|
+
existing: existing_streams.size,
|
|
344
|
+
missing: missing_streams
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
ComponentHealth.new(
|
|
350
|
+
status: :unhealthy,
|
|
351
|
+
message: "Streams check failed: #{e.message}"
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Check outbox health (if enabled)
|
|
356
|
+
#
|
|
357
|
+
# @return [ComponentHealth, nil] Outbox health or nil if disabled
|
|
358
|
+
def check_outbox
|
|
359
|
+
return nil unless NatsPubsub.config.use_outbox
|
|
360
|
+
|
|
361
|
+
model = NatsPubsub.config.outbox_model.constantize
|
|
362
|
+
|
|
363
|
+
pending_count = model.pending.count
|
|
364
|
+
failed_count = model.failed.count
|
|
365
|
+
stale_count = model.stale_publishing(5.minutes.ago).count
|
|
366
|
+
|
|
367
|
+
status = if stale_count > 0 || failed_count > 100
|
|
368
|
+
:unhealthy
|
|
369
|
+
elsif pending_count > 1000
|
|
370
|
+
:degraded
|
|
371
|
+
else
|
|
372
|
+
:healthy
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
ComponentHealth.new(
|
|
376
|
+
status: status,
|
|
377
|
+
message: 'Outbox operational',
|
|
378
|
+
details: {
|
|
379
|
+
pending: pending_count,
|
|
380
|
+
failed: failed_count,
|
|
381
|
+
stale: stale_count
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
rescue StandardError => e
|
|
385
|
+
ComponentHealth.new(
|
|
386
|
+
status: :unhealthy,
|
|
387
|
+
message: "Outbox check failed: #{e.message}"
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Check inbox health (if enabled)
|
|
392
|
+
#
|
|
393
|
+
# @return [ComponentHealth, nil] Inbox health or nil if disabled
|
|
394
|
+
def check_inbox
|
|
395
|
+
return nil unless NatsPubsub.config.use_inbox
|
|
396
|
+
|
|
397
|
+
model = NatsPubsub.config.inbox_model.constantize
|
|
398
|
+
|
|
399
|
+
unprocessed_count = model.unprocessed.count
|
|
400
|
+
failed_count = model.failed.count
|
|
401
|
+
|
|
402
|
+
status = if failed_count > 100
|
|
403
|
+
:unhealthy
|
|
404
|
+
elsif unprocessed_count > 1000
|
|
405
|
+
:degraded
|
|
406
|
+
else
|
|
407
|
+
:healthy
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
ComponentHealth.new(
|
|
411
|
+
status: status,
|
|
412
|
+
message: 'Inbox operational',
|
|
413
|
+
details: {
|
|
414
|
+
unprocessed: unprocessed_count,
|
|
415
|
+
failed: failed_count
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
rescue StandardError => e
|
|
419
|
+
ComponentHealth.new(
|
|
420
|
+
status: :unhealthy,
|
|
421
|
+
message: "Inbox check failed: #{e.message}"
|
|
422
|
+
)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Check connection pool health
|
|
426
|
+
#
|
|
427
|
+
# @return [ComponentHealth] Connection pool health
|
|
428
|
+
def check_connection_pool
|
|
429
|
+
config = NatsPubsub.config
|
|
430
|
+
|
|
431
|
+
ComponentHealth.new(
|
|
432
|
+
status: :healthy,
|
|
433
|
+
message: 'Connection pool configured',
|
|
434
|
+
details: {
|
|
435
|
+
pool_size: config.connection_pool_size,
|
|
436
|
+
pool_timeout: config.connection_pool_timeout
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
rescue StandardError => e
|
|
440
|
+
ComponentHealth.new(
|
|
441
|
+
status: :unhealthy,
|
|
442
|
+
message: "Connection pool check failed: #{e.message}"
|
|
443
|
+
)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Determine overall status from components
|
|
447
|
+
#
|
|
448
|
+
# @param components [Hash] Component health map
|
|
449
|
+
# @return [Symbol] Overall status
|
|
450
|
+
def determine_status(components)
|
|
451
|
+
statuses = components.values.map(&:status)
|
|
452
|
+
|
|
453
|
+
if statuses.all? { |s| s == :healthy }
|
|
454
|
+
:healthy
|
|
455
|
+
elsif statuses.any? { |s| s == :unhealthy }
|
|
456
|
+
# If connection or jetstream is unhealthy, system is unhealthy
|
|
457
|
+
if components[:connection]&.unhealthy? || components[:jetstream]&.unhealthy?
|
|
458
|
+
:unhealthy
|
|
459
|
+
else
|
|
460
|
+
# Otherwise degraded
|
|
461
|
+
:degraded
|
|
462
|
+
end
|
|
463
|
+
else
|
|
464
|
+
:degraded
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
module NatsPubsub
|
|
7
|
+
# Logging helpers that route to the configured logger when available,
|
|
8
|
+
# falling back to Rails.logger or STDOUT.
|
|
9
|
+
module Logging
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def logger
|
|
13
|
+
NatsPubsub.config.logger ||
|
|
14
|
+
(defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
|
15
|
+
default_logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_logger
|
|
19
|
+
@default_logger ||= Logger.new($stdout)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def log(level, msg, tag: nil)
|
|
23
|
+
message = tag ? "[#{tag}] #{msg}" : msg
|
|
24
|
+
logger.public_send(level, message)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def debug(msg, tag: nil)
|
|
28
|
+
log(:debug, msg, tag: tag)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def info(msg, tag: nil)
|
|
32
|
+
log(:info, msg, tag: tag)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def warn(msg, tag: nil)
|
|
36
|
+
log(:warn, msg, tag: tag)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def error(msg, tag: nil)
|
|
40
|
+
log(:error, msg, tag: tag)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
44
|
+
def sanitize_url(url)
|
|
45
|
+
uri = URI.parse(url)
|
|
46
|
+
return url unless uri.user || uri.password
|
|
47
|
+
|
|
48
|
+
userinfo =
|
|
49
|
+
if uri.password # user:pass → keep user, mask pass
|
|
50
|
+
"#{uri.user}:***"
|
|
51
|
+
else # token-only userinfo → mask entirely
|
|
52
|
+
'***'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
host = uri.host || ''
|
|
56
|
+
port = uri.port ? ":#{uri.port}" : ''
|
|
57
|
+
path = uri.path.to_s # omit query on purpose to avoid leaking tokens
|
|
58
|
+
frag = uri.fragment ? "##{uri.fragment}" : ''
|
|
59
|
+
|
|
60
|
+
"#{uri.scheme}://#{userinfo}@#{host}#{port}#{path}#{frag}"
|
|
61
|
+
rescue URI::InvalidURIError
|
|
62
|
+
# Fallback: redact any userinfo before the '@'
|
|
63
|
+
url.gsub(%r{(nats|tls)://([^@/]+)@}i) do
|
|
64
|
+
scheme = Regexp.last_match(1)
|
|
65
|
+
creds = Regexp.last_match(2)
|
|
66
|
+
masked = creds&.include?(':') ? "#{creds&.split(':', 2)&.first}:***" : '***'
|
|
67
|
+
"#{scheme}://#{masked}@"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
71
|
+
end
|
|
72
|
+
end
|