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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +90 -0
- data/MIT-LICENSE +20 -0
- data/README.md +275 -0
- data/RELEASE.md +32 -0
- data/Rakefile +13 -0
- data/SECURITY.md +24 -0
- data/app/assets/config/anomonitor_manifest.js +1 -0
- data/app/assets/stylesheets/anomonitor/application.css +370 -0
- data/app/controllers/anomonitor/anomalies_controller.rb +44 -0
- data/app/controllers/anomonitor/application_controller.rb +21 -0
- data/app/controllers/anomonitor/dashboard_controller.rb +69 -0
- data/app/controllers/anomonitor/jobs_controller.rb +61 -0
- data/app/controllers/anomonitor/metrics_controller.rb +18 -0
- data/app/controllers/anomonitor/mutes_controller.rb +41 -0
- data/app/helpers/anomonitor/application_helper.rb +71 -0
- data/app/models/anomonitor/anomaly.rb +105 -0
- data/app/models/anomonitor/application_record.rb +5 -0
- data/app/models/anomonitor/metric_sample.rb +25 -0
- data/app/models/anomonitor/mute.rb +56 -0
- data/app/views/anomonitor/anomalies/index.html.erb +54 -0
- data/app/views/anomonitor/anomalies/show.html.erb +54 -0
- data/app/views/anomonitor/dashboard/show.html.erb +124 -0
- data/app/views/anomonitor/jobs/index.html.erb +135 -0
- data/app/views/anomonitor/metrics/index.html.erb +33 -0
- data/app/views/anomonitor/mutes/index.html.erb +56 -0
- data/app/views/layouts/anomonitor/application.html.erb +39 -0
- data/config/routes.rb +15 -0
- data/db/migrate/20260807100000_create_anomonitor_metric_samples.rb +17 -0
- data/db/migrate/20260807100001_create_anomonitor_anomalies.rb +23 -0
- data/db/migrate/20260808100000_add_resolved_at_to_anomonitor_anomalies.rb +6 -0
- data/db/migrate/20260808120000_create_anomonitor_mutes.rb +16 -0
- data/lib/anomonitor/collectors/base.rb +25 -0
- data/lib/anomonitor/collectors/delayed_job.rb +55 -0
- data/lib/anomonitor/collectors/schema_drift.rb +144 -0
- data/lib/anomonitor/collectors/sidekiq.rb +47 -0
- data/lib/anomonitor/collectors/solid_queue.rb +57 -0
- data/lib/anomonitor/collectors/table.rb +116 -0
- data/lib/anomonitor/configuration.rb +184 -0
- data/lib/anomonitor/configuration_validator.rb +40 -0
- data/lib/anomonitor/detector.rb +195 -0
- data/lib/anomonitor/digester.rb +79 -0
- data/lib/anomonitor/engine.rb +35 -0
- data/lib/anomonitor/jobs/browser.rb +89 -0
- data/lib/anomonitor/jobs/browsers/delayed_job.rb +143 -0
- data/lib/anomonitor/jobs/browsers/sidekiq.rb +164 -0
- data/lib/anomonitor/jobs/browsers/solid_queue.rb +149 -0
- data/lib/anomonitor/jobs/browsers/table.rb +206 -0
- data/lib/anomonitor/jobs/row.rb +30 -0
- data/lib/anomonitor/metric_point.rb +42 -0
- data/lib/anomonitor/metrics_export.rb +55 -0
- data/lib/anomonitor/notifiers/callable.rb +44 -0
- data/lib/anomonitor/notifiers/composite.rb +33 -0
- data/lib/anomonitor/notifiers/rate_limited.rb +44 -0
- data/lib/anomonitor/notifiers/webhook.rb +108 -0
- data/lib/anomonitor/notifiers.rb +59 -0
- data/lib/anomonitor/poll_lock.rb +67 -0
- data/lib/anomonitor/poller.rb +164 -0
- data/lib/anomonitor/tenancy.rb +46 -0
- data/lib/anomonitor/version.rb +3 -0
- data/lib/anomonitor.rb +52 -0
- data/lib/generators/anomonitor/install/install_generator.rb +27 -0
- data/lib/generators/anomonitor/install/templates/anomonitor.rb +87 -0
- data/lib/tasks/anomonitor_tasks.rake +27 -0
- 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: []
|