anomonitor 0.6.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +90 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +275 -0
  5. data/RELEASE.md +32 -0
  6. data/Rakefile +13 -0
  7. data/SECURITY.md +24 -0
  8. data/app/assets/config/anomonitor_manifest.js +1 -0
  9. data/app/assets/stylesheets/anomonitor/application.css +370 -0
  10. data/app/controllers/anomonitor/anomalies_controller.rb +44 -0
  11. data/app/controllers/anomonitor/application_controller.rb +21 -0
  12. data/app/controllers/anomonitor/dashboard_controller.rb +69 -0
  13. data/app/controllers/anomonitor/jobs_controller.rb +61 -0
  14. data/app/controllers/anomonitor/metrics_controller.rb +18 -0
  15. data/app/controllers/anomonitor/mutes_controller.rb +41 -0
  16. data/app/helpers/anomonitor/application_helper.rb +71 -0
  17. data/app/models/anomonitor/anomaly.rb +105 -0
  18. data/app/models/anomonitor/application_record.rb +5 -0
  19. data/app/models/anomonitor/metric_sample.rb +25 -0
  20. data/app/models/anomonitor/mute.rb +56 -0
  21. data/app/views/anomonitor/anomalies/index.html.erb +54 -0
  22. data/app/views/anomonitor/anomalies/show.html.erb +54 -0
  23. data/app/views/anomonitor/dashboard/show.html.erb +124 -0
  24. data/app/views/anomonitor/jobs/index.html.erb +135 -0
  25. data/app/views/anomonitor/metrics/index.html.erb +33 -0
  26. data/app/views/anomonitor/mutes/index.html.erb +56 -0
  27. data/app/views/layouts/anomonitor/application.html.erb +39 -0
  28. data/config/routes.rb +15 -0
  29. data/db/migrate/20260807100000_create_anomonitor_metric_samples.rb +17 -0
  30. data/db/migrate/20260807100001_create_anomonitor_anomalies.rb +23 -0
  31. data/db/migrate/20260808100000_add_resolved_at_to_anomonitor_anomalies.rb +6 -0
  32. data/db/migrate/20260808120000_create_anomonitor_mutes.rb +16 -0
  33. data/lib/anomonitor/collectors/base.rb +25 -0
  34. data/lib/anomonitor/collectors/delayed_job.rb +55 -0
  35. data/lib/anomonitor/collectors/schema_drift.rb +144 -0
  36. data/lib/anomonitor/collectors/sidekiq.rb +47 -0
  37. data/lib/anomonitor/collectors/solid_queue.rb +57 -0
  38. data/lib/anomonitor/collectors/table.rb +116 -0
  39. data/lib/anomonitor/configuration.rb +184 -0
  40. data/lib/anomonitor/configuration_validator.rb +40 -0
  41. data/lib/anomonitor/detector.rb +195 -0
  42. data/lib/anomonitor/digester.rb +79 -0
  43. data/lib/anomonitor/engine.rb +35 -0
  44. data/lib/anomonitor/jobs/browser.rb +89 -0
  45. data/lib/anomonitor/jobs/browsers/delayed_job.rb +143 -0
  46. data/lib/anomonitor/jobs/browsers/sidekiq.rb +164 -0
  47. data/lib/anomonitor/jobs/browsers/solid_queue.rb +149 -0
  48. data/lib/anomonitor/jobs/browsers/table.rb +206 -0
  49. data/lib/anomonitor/jobs/row.rb +30 -0
  50. data/lib/anomonitor/metric_point.rb +42 -0
  51. data/lib/anomonitor/metrics_export.rb +55 -0
  52. data/lib/anomonitor/notifiers/callable.rb +44 -0
  53. data/lib/anomonitor/notifiers/composite.rb +33 -0
  54. data/lib/anomonitor/notifiers/rate_limited.rb +44 -0
  55. data/lib/anomonitor/notifiers/webhook.rb +108 -0
  56. data/lib/anomonitor/notifiers.rb +59 -0
  57. data/lib/anomonitor/poll_lock.rb +67 -0
  58. data/lib/anomonitor/poller.rb +164 -0
  59. data/lib/anomonitor/tenancy.rb +46 -0
  60. data/lib/anomonitor/version.rb +3 -0
  61. data/lib/anomonitor.rb +52 -0
  62. data/lib/generators/anomonitor/install/install_generator.rb +27 -0
  63. data/lib/generators/anomonitor/install/templates/anomonitor.rb +87 -0
  64. data/lib/tasks/anomonitor_tasks.rake +27 -0
  65. metadata +126 -0
data/lib/anomonitor.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "anomonitor/version"
5
+ require "anomonitor/configuration"
6
+ require "anomonitor/metric_point"
7
+ require "anomonitor/tenancy"
8
+ require "anomonitor/engine"
9
+
10
+ require "anomonitor/collectors/base"
11
+ require "anomonitor/collectors/sidekiq"
12
+ require "anomonitor/collectors/delayed_job"
13
+ require "anomonitor/collectors/solid_queue"
14
+ require "anomonitor/collectors/schema_drift"
15
+ require "anomonitor/collectors/table"
16
+ require "anomonitor/jobs/row"
17
+ require "anomonitor/jobs/browser"
18
+ require "anomonitor/jobs/browsers/sidekiq"
19
+ require "anomonitor/jobs/browsers/delayed_job"
20
+ require "anomonitor/jobs/browsers/solid_queue"
21
+ require "anomonitor/jobs/browsers/table"
22
+ require "anomonitor/notifiers"
23
+ require "anomonitor/notifiers/callable"
24
+ require "anomonitor/notifiers/composite"
25
+ require "anomonitor/notifiers/webhook"
26
+ require "anomonitor/notifiers/rate_limited"
27
+ require "anomonitor/configuration_validator"
28
+ require "anomonitor/metrics_export"
29
+ require "anomonitor/poll_lock"
30
+ require "anomonitor/digester"
31
+ require "anomonitor/detector"
32
+ require "anomonitor/poller"
33
+
34
+ module Anomonitor
35
+ class << self
36
+ def configure
37
+ yield config
38
+ end
39
+
40
+ def config
41
+ @config ||= Configuration.new
42
+ end
43
+
44
+ def reset_config!
45
+ @config = Configuration.new
46
+ end
47
+
48
+ def logger
49
+ @logger ||= (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) || Logger.new($stdout)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Anomonitor::InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ desc "Install Anomonitor initializer"
7
+
8
+ def copy_initializer
9
+ template "anomonitor.rb", "config/initializers/anomonitor.rb"
10
+ end
11
+
12
+ def show_readme
13
+ say <<~MSG
14
+
15
+ Next steps:
16
+ 1. rails anomonitor:install:migrations (or railties:install:migrations FROM=anomonitor)
17
+ 2. rails db:migrate
18
+ 3. mount Anomonitor::Engine => "/anomonitor" in config/routes.rb
19
+ 4. Set ANOMONITOR_WEBHOOK_URL and tune config/initializers/anomonitor.rb
20
+
21
+ Polling:
22
+ - Default poll_mode = :thread (in-process poller)
23
+ - For cron / multi-worker apps set poll_mode = :cron and schedule:
24
+ * * * * * cd /app && bin/rails anomonitor:poll
25
+ MSG
26
+ end
27
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ Anomonitor.configure do |c|
4
+ c.webhook_url = ENV["ANOMONITOR_WEBHOOK_URL"]
5
+ # Absolute origin for Slack/JSON dashboard links (path-only if blank)
6
+ c.dashboard_base_url = ENV["ANOMONITOR_DASHBOARD_BASE_URL"] # e.g. "https://ops.example.com"
7
+
8
+ # Optional: replace built-in HTTP webhook with your transport (e.g. Webhook::Broadcast).
9
+ # c.notifier = ->(anomaly, event:) {
10
+ # Webhook::Broadcast.new(
11
+ # urls: [{ url: ENV.fetch("ANOMONITOR_DEST_URL"), headers: [
12
+ # { name: "Content-Type", value: "application/json" }
13
+ # ] }],
14
+ # message: Anomonitor::Notifiers.payload(anomaly, event: event)
15
+ # ).call
16
+ # }
17
+ c.poll_interval = 60
18
+ c.cooldown = 15 * 60
19
+ c.retention_days = 7
20
+ c.dashboard_path = "/anomonitor"
21
+ # Schema drift is heavier — poll less often than queue metrics (seconds)
22
+ c.schema_drift_interval = 15 * 60
23
+ # Prevent double-poll under multi-worker (pg advisory lock or tmp file lock)
24
+ c.poll_lock = true
25
+
26
+ # Polling:
27
+ # :thread — in-process background poller (default; uses poll_interval)
28
+ # :cron — no background thread; schedule `rails anomonitor:poll`
29
+ #
30
+ # Cron examples when poll_mode = :cron:
31
+ # * * * * * cd /app && bin/rails anomonitor:poll
32
+ # # whenever: every 1.minute do rake "anomonitor:poll" end
33
+ c.poll_mode = :thread
34
+ # c.poll_mode = :cron
35
+
36
+ # Protect the dashboard (runs in Anomonitor controllers via instance_exec):
37
+ # c.authenticate = -> {
38
+ # authenticate_or_request_with_http_basic("Anomonitor") do |user, pass|
39
+ # ActiveSupport::SecurityUtils.secure_compare(user, ENV.fetch("ANOMONITOR_USER")) &&
40
+ # ActiveSupport::SecurityUtils.secure_compare(pass, ENV.fetch("ANOMONITOR_PASSWORD"))
41
+ # end
42
+ # }
43
+ # Or reuse host auth, e.g.:
44
+ # c.authenticate = -> { redirect_to main_app.root_path unless current_user&.admin? }
45
+
46
+ c.collectors.sidekiq = true
47
+ c.collectors.delayed_job = true
48
+ c.collectors.solid_queue = true
49
+ c.collectors.schema_drift = false
50
+
51
+ # Multi-tenancy (Apartment / schema-per-tenant) — uncomment to enable:
52
+ # c.tenants = -> { CustomerTenant.pluck(:name) }
53
+ # c.exclude_tenants = %w[public]
54
+ # c.tenant_switch = ->(name, &block) { Apartment::Tenant.switch(name, &block) }
55
+ # c.collectors.schema_drift = true
56
+ # c.schema_drift_exclude = %w[schema_migrations ar_internal_metadata anomonitor_* a* conv_*]
57
+ # c.alert :missing_tables, max: 0
58
+ # c.alert :extra_tables, max: 0
59
+ # c.alert :missing_columns, max: 0
60
+ # c.alert :extra_columns, max: 0
61
+
62
+ # Custom job table (status-based):
63
+ # c.table :customer_jobs do |t|
64
+ # t.model = "Job"
65
+ # t.timestamp = :created_at
66
+ # t.status = :status
67
+ # t.active = %w[pending running]
68
+ # end
69
+
70
+ # Cross-tenant Delayed Job index (e.g. public.supp_jobs via a host AR model):
71
+ # c.table :supp_jobs do |t|
72
+ # t.model = "SuppJob"
73
+ # t.style = :delayed_job
74
+ # t.tenant = :tenant
75
+ # end
76
+
77
+ c.alert :queue_depth, max: 1_000 # aggregate (untagged) + any matching points
78
+ # Stricter per-queue Sidekiq depths (only points tagged with a queue name):
79
+ c.alert :queue_depth, max: 250, match: { queue: true }
80
+ c.alert :failed, max: 50
81
+ c.alert :latency, max: 120, match: { queue: true }
82
+ c.alert :growth_spike, window: 5 * 60, multiplier: 3.0
83
+
84
+ # Optional: batch webhook noise into a digest every N seconds
85
+ # c.digest_interval = 3600
86
+ # c.notifier_rate_limit = 30
87
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :anomonitor do
4
+ desc "Run a single Anomonitor collection + detection tick (use with cron when poll_mode=:cron)"
5
+ task poll: :environment do
6
+ points =
7
+ if defined?(Apartment::Tenant)
8
+ home = Array(Anomonitor.config.exclude_tenants).first || "public"
9
+ Apartment::Tenant.switch(home) { Anomonitor::Poller.instance.tick }
10
+ else
11
+ Anomonitor::Poller.instance.tick
12
+ end
13
+
14
+ puts "Collected #{Array(points).size} metric points (poll_mode=#{Anomonitor.config.poll_mode})"
15
+ end
16
+
17
+ desc "Retry failed anomaly webhook deliveries"
18
+ task retry_webhooks: :environment do
19
+ scope = Anomonitor::Anomaly.webhook_failed.order(created_at: :desc)
20
+ total = scope.count
21
+ ok = 0
22
+ scope.find_each do |anomaly|
23
+ ok += 1 if anomaly.retry_webhook!
24
+ end
25
+ puts "Retried #{total} failed webhooks — #{ok} delivered"
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anomonitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - flori-s
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ description: Mountable Rails engine that collects Sidekiq, Delayed Job, Solid Queue,
28
+ and custom table metrics, detects threshold and growth spikes, sends webhook alerts,
29
+ and provides an ops dashboard.
30
+ email:
31
+ - 150121780+flori-s@users.noreply.github.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - MIT-LICENSE
38
+ - README.md
39
+ - RELEASE.md
40
+ - Rakefile
41
+ - SECURITY.md
42
+ - app/assets/config/anomonitor_manifest.js
43
+ - app/assets/stylesheets/anomonitor/application.css
44
+ - app/controllers/anomonitor/anomalies_controller.rb
45
+ - app/controllers/anomonitor/application_controller.rb
46
+ - app/controllers/anomonitor/dashboard_controller.rb
47
+ - app/controllers/anomonitor/jobs_controller.rb
48
+ - app/controllers/anomonitor/metrics_controller.rb
49
+ - app/controllers/anomonitor/mutes_controller.rb
50
+ - app/helpers/anomonitor/application_helper.rb
51
+ - app/models/anomonitor/anomaly.rb
52
+ - app/models/anomonitor/application_record.rb
53
+ - app/models/anomonitor/metric_sample.rb
54
+ - app/models/anomonitor/mute.rb
55
+ - app/views/anomonitor/anomalies/index.html.erb
56
+ - app/views/anomonitor/anomalies/show.html.erb
57
+ - app/views/anomonitor/dashboard/show.html.erb
58
+ - app/views/anomonitor/jobs/index.html.erb
59
+ - app/views/anomonitor/metrics/index.html.erb
60
+ - app/views/anomonitor/mutes/index.html.erb
61
+ - app/views/layouts/anomonitor/application.html.erb
62
+ - config/routes.rb
63
+ - db/migrate/20260807100000_create_anomonitor_metric_samples.rb
64
+ - db/migrate/20260807100001_create_anomonitor_anomalies.rb
65
+ - db/migrate/20260808100000_add_resolved_at_to_anomonitor_anomalies.rb
66
+ - db/migrate/20260808120000_create_anomonitor_mutes.rb
67
+ - lib/anomonitor.rb
68
+ - lib/anomonitor/collectors/base.rb
69
+ - lib/anomonitor/collectors/delayed_job.rb
70
+ - lib/anomonitor/collectors/schema_drift.rb
71
+ - lib/anomonitor/collectors/sidekiq.rb
72
+ - lib/anomonitor/collectors/solid_queue.rb
73
+ - lib/anomonitor/collectors/table.rb
74
+ - lib/anomonitor/configuration.rb
75
+ - lib/anomonitor/configuration_validator.rb
76
+ - lib/anomonitor/detector.rb
77
+ - lib/anomonitor/digester.rb
78
+ - lib/anomonitor/engine.rb
79
+ - lib/anomonitor/jobs/browser.rb
80
+ - lib/anomonitor/jobs/browsers/delayed_job.rb
81
+ - lib/anomonitor/jobs/browsers/sidekiq.rb
82
+ - lib/anomonitor/jobs/browsers/solid_queue.rb
83
+ - lib/anomonitor/jobs/browsers/table.rb
84
+ - lib/anomonitor/jobs/row.rb
85
+ - lib/anomonitor/metric_point.rb
86
+ - lib/anomonitor/metrics_export.rb
87
+ - lib/anomonitor/notifiers.rb
88
+ - lib/anomonitor/notifiers/callable.rb
89
+ - lib/anomonitor/notifiers/composite.rb
90
+ - lib/anomonitor/notifiers/rate_limited.rb
91
+ - lib/anomonitor/notifiers/webhook.rb
92
+ - lib/anomonitor/poll_lock.rb
93
+ - lib/anomonitor/poller.rb
94
+ - lib/anomonitor/tenancy.rb
95
+ - lib/anomonitor/version.rb
96
+ - lib/generators/anomonitor/install/install_generator.rb
97
+ - lib/generators/anomonitor/install/templates/anomonitor.rb
98
+ - lib/tasks/anomonitor_tasks.rake
99
+ homepage: https://github.com/flori-s/anomonitor
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ homepage_uri: https://github.com/flori-s/anomonitor
104
+ source_code_uri: https://github.com/flori-s/anomonitor
105
+ changelog_uri: https://github.com/flori-s/anomonitor/blob/main/CHANGELOG.md
106
+ rubygems_mfa_required: 'true'
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.0.0
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.2.33
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Monitor job queues and custom job tables for anomalies
126
+ test_files: []