solid_queue_guard 1.4.0 → 1.5.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: 0f68d16b641aa34f7cf2cda0731e1f279d4078466271f25bdac801d812a1c050
4
- data.tar.gz: d9765f83f9e100ec1508603cc6e6c461ca5835a925d32ef30b993d122eafb6ce
3
+ metadata.gz: 5d166e1caf2f2d8d5b70a3e3286eae29fcc54d86aa13d75d98d22c66948461f3
4
+ data.tar.gz: cf506ca78e106a3471afb9d7b5d76c316390d32c534ca57a0ea14bd94e4212e0
5
5
  SHA512:
6
- metadata.gz: 24f148012d857a399cb27ceab0ca17ed9b09e4215610e82aa96d9bbb2fa9b84b05b85e63723ffbe7edd57d65cb8c3ed81305ee0e2650ffb798aeffd55bbc1c11
7
- data.tar.gz: bcd5ab443291828fbf19100ded2010d54d5d57cf764390623ca5e67d4291fb80c1fca37e5498ada656dfc95e184d8484e0a17e4763a0052f634a186980961bff
6
+ metadata.gz: 4ebab9e0becb3c9f275b2797e6e51eb71fc79fe8af88a1c088e307840ec11568fbe975f0b9015168aaf0e6eb1cf6eff05e3ab8e59cad1733d6cd650003d14941
7
+ data.tar.gz: 78cfc87e754be0ae63ee59ca41c79d905a37af6b4c1b7c2b67f47b51ddb55813368b4048855a02cdb10922717e6151642c919ff1c765dd5a6bfefbb0eb548449
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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
+ ## [1.5.0] - 2026-08-04
9
+
10
+ ### Added
11
+
12
+ - Operational Datadog metrics for Solid Queue depth and job events (opt-in):
13
+ - `config.emit_depth_metrics` — recurring gauges `solid_queue.ready.count`,
14
+ `solid_queue.ready.oldest_age_seconds`, `failed` / `claimed` / `scheduled` counts
15
+ via `SolidQueueGuard::Metrics::EmitDepthJob` (same DB source as Mission Control)
16
+ - `config.emit_event_metrics` — Active Job + Solid Queue lifecycle counters
17
+ (`solid_queue.jobs.*`, `solid_queue.job.duration_ms`, `solid_queue.process.active`)
18
+ - `config.statsd_service_name` (falls back to `DD_SERVICE` / `SOLID_QUEUE_GUARD_SERVICE`)
19
+ - Generator: `bin/rails generate solid_queue_guard:metrics` wires flags + `recurring.yml`
20
+ - Requires optional gem `dogstatsd-ruby` (`Datadog::Statsd`). Distinct from
21
+ `config.metrics_backends` health-check export (`solid_queue.guard.*`).
22
+
8
23
  ## [1.4.0] - 2026-08-03
9
24
 
10
25
  ### Changed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- solid_queue_guard (1.4.0)
4
+ solid_queue_guard (1.5.0)
5
5
  actionpack (>= 7.1, < 9.0)
6
6
  activejob (>= 7.1, < 9.0)
7
7
  activerecord (>= 7.1, < 9.0)
data/README.md CHANGED
@@ -124,6 +124,19 @@ Requires `mission_control-jobs` and an asset pipeline (Propshaft or Sprockets).
124
124
 
125
125
  ---
126
126
 
127
+ ## Operational Datadog metrics
128
+
129
+ Opt-in continuous depth gauges (same DB source as Mission Control) plus Active Job event counters:
130
+
131
+ ```bash
132
+ bundle add dogstatsd-ruby
133
+ bin/rails generate solid_queue_guard:metrics
134
+ ```
135
+
136
+ Emits `solid_queue.ready.count`, `solid_queue.ready.oldest_age_seconds`, failed/claimed/scheduled counts, and `solid_queue.jobs.*` — see [docs/configuration.md](docs/configuration.md).
137
+
138
+ ---
139
+
127
140
  ## Public API (v1.0+)
128
141
 
129
142
  Stable until `2.0` — [semantic versioning](https://semver.org/):
@@ -135,6 +148,8 @@ Stable until `2.0` — [semantic versioning](https://semver.org/):
135
148
  | `solid_queue_guard:report` | Full diagnostic report |
136
149
  | `mount SolidQueueGuard::Engine` | HTTP health endpoint |
137
150
  | `config.integrate_mission_control` | Guard tab (requires `mission_control-jobs`) |
151
+ | `config.emit_depth_metrics` / `emit_event_metrics` | Operational Datadog gauges/counters (requires `dogstatsd-ruby`) |
152
+ | `bin/rails generate solid_queue_guard:metrics` | Wire depth recurring job + event flags |
138
153
 
139
154
  Internal check classes and registry are `@api private`.
140
155
 
@@ -8,7 +8,7 @@ module SolidQueueGuard
8
8
  when 'healthy', 'pass' then 'is-success'
9
9
  when 'degraded', 'warn' then 'is-warning'
10
10
  when 'unhealthy', 'fail' then 'is-danger'
11
- else 'is-light'
11
+ else 'is-dark' # skip / unknown — is-light is unreadable on Mission Control dark theme
12
12
  end
13
13
  end
14
14
 
@@ -26,4 +26,10 @@ SolidQueueGuard.configure do |config|
26
26
  # config.on_status_change = ->(previous, current, report) { ... }
27
27
  # config.notify_with = [:rails_logger, :slack, :datadog, :webhook]
28
28
  # config.metrics_backends = [:statsd, :prometheus, :opentelemetry]
29
+
30
+ # Operational Datadog metrics (depth gauges + job event counters).
31
+ # Requires dogstatsd-ruby. Enable with: bin/rails generate solid_queue_guard:metrics
32
+ # config.emit_depth_metrics = true
33
+ # config.emit_event_metrics = true
34
+ # config.statsd_service_name = 'my-service' # or set DD_SERVICE
29
35
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module SolidQueueGuard
6
+ module Generators
7
+ class MetricsGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ RECURRING_SNIPPET = <<~YAML
11
+ emit_solid_queue_depth_metrics:
12
+ class: SolidQueueGuard::Metrics::EmitDepthJob
13
+ queue: solid_queue_recurring
14
+ schedule: every 15 seconds
15
+ YAML
16
+
17
+ CONFIG_SNIPPET = <<~RUBY
18
+
19
+ # Operational Datadog metrics (requires dogstatsd-ruby)
20
+ config.emit_depth_metrics = true
21
+ config.emit_event_metrics = true
22
+ # config.statsd_service_name = 'my-service' # or set DD_SERVICE
23
+ RUBY
24
+
25
+ desc 'Enables Solid Queue operational metrics (depth gauges + event counters) and wires recurring.yml'
26
+
27
+ def enable_config_flags
28
+ initializer = 'config/initializers/solid_queue_guard.rb'
29
+ full_path = File.expand_path(initializer, destination_root)
30
+
31
+ unless File.exist?(full_path)
32
+ say_status :error, "#{initializer} not found — run solid_queue_guard:install first", :red
33
+ return
34
+ end
35
+
36
+ contents = File.read(full_path)
37
+ if contents.include?('emit_depth_metrics')
38
+ say_status :identical, "#{initializer} already enables depth metrics", :blue
39
+ return
40
+ end
41
+
42
+ inject_into_file initializer, before: /\nend\s*\z/ do
43
+ CONFIG_SNIPPET
44
+ end
45
+ end
46
+
47
+ def add_recurring_task
48
+ relative = 'config/recurring.yml'
49
+ path = File.expand_path(relative, destination_root)
50
+
51
+ unless File.exist?(path)
52
+ say_status :skip, "#{relative} not found", :yellow
53
+ return
54
+ end
55
+
56
+ content = File.read(path)
57
+ if content.include?('emit_solid_queue_depth_metrics')
58
+ say_status :identical, "#{relative} already has emit_solid_queue_depth_metrics", :blue
59
+ return
60
+ end
61
+
62
+ updated = insert_recurring_snippet(content)
63
+ File.write(path, updated)
64
+ say_status :insert, "#{relative} (emit_solid_queue_depth_metrics)", :green
65
+ end
66
+
67
+ def remind_dependency
68
+ say <<~MSG
69
+
70
+ Add to your Gemfile if missing:
71
+ gem 'dogstatsd-ruby'
72
+
73
+ Ensure a Solid Queue worker consumes the `solid_queue_recurring` queue,
74
+ then deploy and verify:
75
+ solid_queue.ready.count{service:<your-service>}
76
+ MSG
77
+ end
78
+
79
+ private
80
+
81
+ def insert_recurring_snippet(content)
82
+ indented = RECURRING_SNIPPET.lines.map { |line| line == "\n" ? line : " #{line}" }.join
83
+
84
+ if content.match?(/^default:/)
85
+ content.sub(/^(default:(?: &\w+)?\n(?: .*\n)*)/) do |block|
86
+ "#{block.rstrip}\n#{indented}"
87
+ end
88
+ else
89
+ "#{content.rstrip}\n\n#{RECURRING_SNIPPET}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -22,7 +22,10 @@ module SolidQueueGuard
22
22
  :checks,
23
23
  :degraded_http_status,
24
24
  :unhealthy_http_status,
25
- :on_status_change
25
+ :on_status_change,
26
+ :emit_depth_metrics,
27
+ :emit_event_metrics,
28
+ :statsd_service_name
26
29
 
27
30
  def initialize
28
31
  @enabled = true
@@ -42,6 +45,9 @@ module SolidQueueGuard
42
45
  @degraded_http_status = :ok
43
46
  @unhealthy_http_status = :service_unavailable
44
47
  @on_status_change = nil
48
+ @emit_depth_metrics = false
49
+ @emit_event_metrics = false
50
+ @statsd_service_name = nil
45
51
  end
46
52
 
47
53
  def validate!
@@ -51,5 +51,11 @@ module SolidQueueGuard
51
51
  SolidQueueGuard::MissionControl::Integration.install_navigation!
52
52
  end
53
53
  end
54
+
55
+ initializer 'solid_queue_guard.event_metrics' do
56
+ config.after_initialize do
57
+ SolidQueueGuard::Metrics::EventInstrumentation.install!
58
+ end
59
+ end
54
60
  end
55
61
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Metrics
5
+ # Snapshot gauges answering "are jobs stuck right now?"
6
+ # (same DB source as Mission Control /jobs).
7
+ module DepthEmitter
8
+ module_function
9
+
10
+ def emit!
11
+ return unless enabled?
12
+
13
+ emit_ready_gauges!
14
+ emit_execution_gauges!
15
+ statsd.flush if statsd.respond_to?(:flush)
16
+ end
17
+
18
+ def enabled?
19
+ SolidQueueGuard.config.emit_depth_metrics &&
20
+ %w[production staging].include?(Rails.env.to_s)
21
+ end
22
+
23
+ def statsd
24
+ DogstatsdClient.statsd
25
+ end
26
+
27
+ def emit_ready_gauges!
28
+ ready_counts = SolidQueue::ReadyExecution.group(:queue_name).count
29
+ oldest_by_queue = SolidQueue::ReadyExecution.group(:queue_name).minimum(:created_at)
30
+
31
+ if ready_counts.empty?
32
+ statsd.gauge('solid_queue.ready.count', 0)
33
+ statsd.gauge('solid_queue.ready.oldest_age_seconds', 0)
34
+ return
35
+ end
36
+
37
+ emit_ready_counts!(ready_counts)
38
+ emit_ready_ages!(oldest_by_queue)
39
+ end
40
+
41
+ def emit_ready_counts!(ready_counts)
42
+ ready_counts.each do |queue_name, count|
43
+ statsd.gauge('solid_queue.ready.count', count, tags: ["queue:#{queue_name}"])
44
+ end
45
+ end
46
+
47
+ def emit_ready_ages!(oldest_by_queue)
48
+ now = Time.current
49
+ oldest_by_queue.each do |queue_name, created_at|
50
+ age = created_at ? [(now - created_at).to_i, 0].max : 0
51
+ statsd.gauge('solid_queue.ready.oldest_age_seconds', age, tags: ["queue:#{queue_name}"])
52
+ end
53
+ end
54
+
55
+ def emit_execution_gauges!
56
+ statsd.gauge('solid_queue.failed.count', SolidQueue::FailedExecution.count)
57
+ statsd.gauge('solid_queue.claimed.count', SolidQueue::ClaimedExecution.count)
58
+ statsd.gauge('solid_queue.scheduled.count', SolidQueue::ScheduledExecution.count)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Metrics
5
+ # Shared Datadog StatsD client for operational gauges/counters.
6
+ # Distinct from Metrics::Statsd (raw UDP export of guard check status).
7
+ module DogstatsdClient
8
+ module_function
9
+
10
+ def statsd
11
+ @statsd ||= build_client
12
+ end
13
+
14
+ def reset!
15
+ @statsd = nil
16
+ remove_instance_variable(:@available) if instance_variable_defined?(:@available)
17
+ end
18
+
19
+ def available?
20
+ return @available if instance_variable_defined?(:@available)
21
+
22
+ @available = OptionalDependency.require!('datadog/statsd', 'dogstatsd-ruby')
23
+ end
24
+
25
+ def service_name
26
+ SolidQueueGuard.config.statsd_service_name.presence ||
27
+ ENV['DD_SERVICE'].presence ||
28
+ ENV['SOLID_QUEUE_GUARD_SERVICE'].presence
29
+ end
30
+
31
+ def default_tags
32
+ tags = ["env:#{Rails.env}"]
33
+ tags << "service:#{service_name}" if service_name
34
+ tags
35
+ end
36
+
37
+ def job_tags(job)
38
+ ["job_class:#{job.class.name}", "queue:#{job.queue_name}"]
39
+ end
40
+
41
+ def process_tags(kind, process)
42
+ ["kind:#{kind}", "process:#{process.name}"]
43
+ end
44
+
45
+ def build_client
46
+ return NullStatsd.new unless available?
47
+
48
+ ::Datadog::Statsd.new(tags: default_tags)
49
+ end
50
+ private_class_method :build_client
51
+
52
+ # No-op client when dogstatsd-ruby is not installed.
53
+ class NullStatsd
54
+ def increment(*) = nil
55
+ def gauge(*) = nil
56
+ def timing(*) = nil
57
+ def flush = nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Metrics
5
+ # Host apps have ApplicationJob; this gem job inherits ActiveJob::Base directly.
6
+ class EmitDepthJob < ::ActiveJob::Base # rubocop:disable Rails/ApplicationJob
7
+ queue_as :solid_queue_recurring
8
+
9
+ def perform
10
+ DepthEmitter.emit!
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Metrics
5
+ # Subscribes to Active Job notifications and Solid Queue lifecycle hooks.
6
+ # Opt-in via config.emit_event_metrics = true.
7
+ module EventInstrumentation
8
+ class << self
9
+ def install!
10
+ return if @installed
11
+ return unless SolidQueueGuard.config.emit_event_metrics
12
+ return unless %w[production staging].include?(Rails.env.to_s)
13
+ return unless DogstatsdClient.available?
14
+
15
+ subscribe_active_job!
16
+ subscribe_solid_queue_lifecycle!
17
+ @installed = true
18
+ end
19
+
20
+ def reset!
21
+ @installed = false
22
+ end
23
+
24
+ private
25
+
26
+ def subscribe_active_job!
27
+ subscribe_enqueue!
28
+ subscribe_perform!
29
+ subscribe_retry_lifecycle!
30
+ end
31
+
32
+ def subscribe_enqueue!
33
+ ActiveSupport::Notifications.subscribe('enqueue.active_job') do |event|
34
+ job = event.payload.fetch(:job)
35
+ DogstatsdClient.statsd.increment(
36
+ 'solid_queue.jobs.enqueued',
37
+ tags: DogstatsdClient.job_tags(job)
38
+ )
39
+ end
40
+ end
41
+
42
+ def subscribe_perform!
43
+ ActiveSupport::Notifications.subscribe('perform.active_job') do |event|
44
+ job = event.payload.fetch(:job)
45
+ status = event.payload[:exception_object] || event.payload[:exception] ? 'error' : 'success'
46
+ tags = [*DogstatsdClient.job_tags(job), "status:#{status}"]
47
+
48
+ DogstatsdClient.statsd.increment('solid_queue.jobs.performed', tags:)
49
+ DogstatsdClient.statsd.timing('solid_queue.job.duration_ms', event.duration, tags:)
50
+ end
51
+ end
52
+
53
+ def subscribe_retry_lifecycle!
54
+ %w[enqueue_retry retry_stopped discard].each do |event_name|
55
+ ActiveSupport::Notifications.subscribe("#{event_name}.active_job") do |event|
56
+ job = event.payload.fetch(:job)
57
+ DogstatsdClient.statsd.increment(
58
+ "solid_queue.jobs.#{event_name}",
59
+ tags: DogstatsdClient.job_tags(job)
60
+ )
61
+ end
62
+ end
63
+ end
64
+
65
+ def subscribe_solid_queue_lifecycle!
66
+ {
67
+ worker: %i[on_worker_start on_worker_stop],
68
+ dispatcher: %i[on_dispatcher_start on_dispatcher_stop],
69
+ scheduler: %i[on_scheduler_start on_scheduler_stop]
70
+ }.each do |kind, (start_hook, stop_hook)|
71
+ SolidQueue.public_send(start_hook) do |process|
72
+ DogstatsdClient.statsd.gauge(
73
+ 'solid_queue.process.active',
74
+ 1,
75
+ tags: DogstatsdClient.process_tags(kind, process)
76
+ )
77
+ end
78
+ SolidQueue.public_send(stop_hook) do |process|
79
+ DogstatsdClient.statsd.gauge(
80
+ 'solid_queue.process.active',
81
+ 0,
82
+ tags: DogstatsdClient.process_tags(kind, process)
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueGuard
4
- VERSION = '1.4.0'
4
+ VERSION = '1.5.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Pissardo
@@ -325,6 +325,7 @@ files:
325
325
  - lib/generators/solid_queue_guard/install/install_generator.rb
326
326
  - lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb
327
327
  - lib/generators/solid_queue_guard/install/templates/solid_queue_guard.yml
328
+ - lib/generators/solid_queue_guard/metrics/metrics_generator.rb
328
329
  - lib/solid_queue_guard.rb
329
330
  - lib/solid_queue_guard/check/result.rb
330
331
  - lib/solid_queue_guard/checks/base.rb
@@ -365,6 +366,10 @@ files:
365
366
  - lib/solid_queue_guard/health/cache.rb
366
367
  - lib/solid_queue_guard/health/payload.rb
367
368
  - lib/solid_queue_guard/http_status_policy.rb
369
+ - lib/solid_queue_guard/metrics/depth_emitter.rb
370
+ - lib/solid_queue_guard/metrics/dogstatsd_client.rb
371
+ - lib/solid_queue_guard/metrics/emit_depth_job.rb
372
+ - lib/solid_queue_guard/metrics/event_instrumentation.rb
368
373
  - lib/solid_queue_guard/metrics/exporter.rb
369
374
  - lib/solid_queue_guard/metrics/open_telemetry.rb
370
375
  - lib/solid_queue_guard/metrics/prometheus.rb