deployangel 0.1.4 → 0.1.6

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: 633da2fd342948cb1c2b6f845f05b9a5262d9674a2f7d40f05532995f0751d7e
4
- data.tar.gz: 63327a9c5226d6032f2d7fdc6e432fee07a2cec9bf9d979cd96905b8d362e22c
3
+ metadata.gz: 95e80de544815ec904e01198f948c81c722ddadb005f6e87761827ac1cb3a362
4
+ data.tar.gz: 6175861126a88ed4bc09bdaf600c03e1ce5cd88fa3361c53d7c761ff7a066b38
5
5
  SHA512:
6
- metadata.gz: 70be2af92e45071c06cf09d60ec43a301db1803eabe917f294927b73d82917283fb9f6cb81af1b6e2daf522b65c52dc9985f1c75c3a16f006157b797c6e2bd15
7
- data.tar.gz: 40518de68db5c3588bccb02e90068dbcc378b342a3ca0325d6a59c253cfa5daead535a54203247d9f58b5910f6d29b3ab2bdec8b23144515141754478bc5417a
6
+ metadata.gz: 9d20de4305540d12d4b24223c7f4eda020b0855b8205cbeac03c51bf8befc701cb783a1e4600c3781b7d15b906a739aa63988bacc4864bc15a286e32c0138405
7
+ data.tar.gz: 38682cdc5941ad48af3ed324356d5bf71309bc5fe2c41fd0c582198ce0315c1863575f5896b13516b634cf6d56e69aee1124a3491c2fa4aae28c49f4b6a77a59
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6 (2026-10-04)
4
+
5
+ - Requests to Rails' health check (`rails/health#show`, at `/up` by default)
6
+ are no longer recorded, wherever it's mounted. Load balancers and uptime
7
+ monitors call it all the time and it always answers fast, so it made apps
8
+ look busier and healthier than their real pages. DeployAngel already
9
+ leaves out `/up` from older agents.
10
+
11
+ ## 0.1.5 (2026-10-03)
12
+
13
+ - Declared recurring jobs from sidekiq-cron and sidekiq-scheduler, alongside
14
+ Solid Queue's. sidekiq-cron jobs come from its schedule file
15
+ (`config/schedule.yml` unless configured otherwise); sidekiq-scheduler jobs
16
+ from the `:scheduler: :schedule:` section of Sidekiq's config file (the
17
+ `-C` file in the Procfile's sidekiq command, else `config/sidekiq.yml`),
18
+ with that environment's section applied. A repeating `every` or `interval`
19
+ is sent as `every`. Both carry the process's local time zone, which the
20
+ schedulers read a schedule in when it names none. Disabled jobs, jobs for
21
+ other environments, and one-off `at` and `in` jobs are left out. Jobs that
22
+ exist only in Redis aren't read.
23
+
3
24
  ## 0.1.4 (2026-10-03)
4
25
 
5
26
  - A Solid Queue recurring task that runs a `command:` instead of a job class
data/README.md CHANGED
@@ -64,9 +64,10 @@ deploy, and the dashboard says so.
64
64
  - Exceptions: a stable fingerprint, the exception class, a sanitized message
65
65
  (numbers, IDs, emails, and quoted values removed), and application frames
66
66
  only.
67
- - Once per process: the route table, job classes, Solid Queue recurring
68
- schedules, critical flows, and file digests (relative paths and hashes, never
69
- file contents) so DeployAngel can tell which routes changed in a release.
67
+ - Once per process: the route table, job classes, recurring schedules
68
+ declared for Solid Queue, sidekiq-cron, or sidekiq-scheduler, critical
69
+ flows, and file digests (relative paths and hashes, never file contents) so
70
+ DeployAngel can tell which routes changed in a release.
70
71
  Disable digests with `DEPLOYANGEL_FILE_DIGESTS=false`.
71
72
 
72
73
  It does not send request bodies, parameters, headers, cookies, SQL, logs, or
@@ -7,6 +7,10 @@ module DeployAngel
7
7
  # never the raw path, which keeps IDs out of route keys.
8
8
  class Http
9
9
  FORMAT_SUFFIX = "(.:format)"
10
+ # Rails' health check (/up by default). Load balancers and uptime
11
+ # monitors call it all the time and it always answers fast, so
12
+ # counting it would make any app look busy and healthy.
13
+ HEALTH_CHECK_CONTROLLER = "rails/health"
10
14
 
11
15
  def initialize(app)
12
16
  @app = app
@@ -31,6 +35,8 @@ module DeployAngel
31
35
 
32
36
  private
33
37
  def record(env, status, started, unhandled:, exception: nil)
38
+ return if env["action_dispatch.request.path_parameters"]&.dig(:controller) == HEALTH_CHECK_CONTROLLER
39
+
34
40
  route = route_key(env)
35
41
  # Unrouted successes are static files and similar; unrouted
36
42
  # errors (such as routing 404s) still count.
@@ -58,8 +58,15 @@ module DeployAngel
58
58
  []
59
59
  end
60
60
 
61
- # Solid Queue recurring tasks for the current environment.
61
+ # Declared recurring jobs from Solid Queue, sidekiq-cron, and
62
+ # sidekiq-scheduler. Each source is read on its own, so a file that
63
+ # can't be read leaves the others' schedules in place.
62
64
  def schedules
65
+ solid_queue_schedules + sidekiq_cron_schedules + sidekiq_scheduler_schedules
66
+ end
67
+
68
+ # Solid Queue recurring tasks for the current environment.
69
+ def solid_queue_schedules
63
70
  path = File.join(@root, "config", "recurring.yml")
64
71
  return [] unless File.file?(path)
65
72
 
@@ -77,6 +84,104 @@ module DeployAngel
77
84
  []
78
85
  end
79
86
 
87
+ # sidekiq-cron jobs from its schedule file (config/schedule.yml unless
88
+ # configured otherwise), which it loads when Sidekiq starts. Jobs
89
+ # created in code live only in Redis, which the agent doesn't read.
90
+ def sidekiq_cron_schedules
91
+ return [] unless defined?(::Sidekiq::Cron)
92
+
93
+ path = yaml_path(sidekiq_cron_schedule_file) or return []
94
+ jobs = load_yaml(path)
95
+ jobs = jobs.map { |name, job| job.is_a?(Hash) ? job.merge("name" => name) : job } if jobs.is_a?(Hash)
96
+ Array(jobs).filter_map do |job|
97
+ next unless job.is_a?(Hash) && job["cron"] && job["status"].to_s != "disabled"
98
+
99
+ { "key" => job["name"].to_s, "class" => (job["klass"] || job["class"])&.to_s, "schedule" => job["cron"].to_s,
100
+ "source" => "sidekiq_cron", "time_zone" => local_time_zone }
101
+ end
102
+ rescue StandardError
103
+ []
104
+ end
105
+
106
+ def sidekiq_cron_schedule_file
107
+ configured = ::Sidekiq::Cron.configuration.cron_schedule_file if ::Sidekiq::Cron.respond_to?(:configuration)
108
+ File.expand_path(configured || "config/schedule.yml", @root)
109
+ rescue StandardError
110
+ File.join(@root, "config", "schedule.yml")
111
+ end
112
+
113
+ # sidekiq-scheduler jobs from Sidekiq's config file, where it reads
114
+ # them under :scheduler: :schedule: (or :schedule: in older versions).
115
+ # A cron runs at set times; every and interval repeat from when the
116
+ # scheduler starts, which a deploy restarts. One-off at and in jobs
117
+ # aren't recurring, so they're left out.
118
+ def sidekiq_scheduler_schedules
119
+ return [] unless defined?(::SidekiqScheduler)
120
+
121
+ path = sidekiq_config_file or return []
122
+ config = normalize_keys(load_yaml(path) || {})
123
+ config = config.merge(config.delete(::Rails.env.to_s) || {}) if config.is_a?(Hash)
124
+ jobs = config.dig("scheduler", "schedule") || config["schedule"]
125
+ return [] unless jobs.is_a?(Hash)
126
+
127
+ jobs.filter_map do |name, job|
128
+ next unless job.is_a?(Hash) && job["enabled"] != false && scheduled_in_this_environment?(job)
129
+
130
+ type = %w[cron every at in interval].find { |key| Array(job[key]).first.to_s.strip != "" }
131
+ next unless %w[cron every interval].include?(type)
132
+
133
+ value = Array(job[type]).first.to_s
134
+ schedule = { "key" => name.to_s, "class" => (job["class"] || name).to_s, "source" => "sidekiq_scheduler",
135
+ "time_zone" => local_time_zone }
136
+ type == "cron" ? schedule.merge("schedule" => value) : schedule.merge("schedule" => nil, "every" => value)
137
+ end
138
+ rescue StandardError
139
+ []
140
+ end
141
+
142
+ # The file Sidekiq loads: the -C path in the Procfile's sidekiq
143
+ # command, else config/sidekiq.yml.
144
+ def sidekiq_config_file
145
+ procfile = File.join(@root, "Procfile")
146
+ named = File.read(procfile)[/\bsidekiq\b[^\n]*?\s(?:-C|--config)[\s=]+(\S+)/, 1] if File.file?(procfile)
147
+ named ? yaml_path(File.expand_path(named, @root)) : yaml_path(File.join(@root, "config", "sidekiq.yml"))
148
+ rescue StandardError
149
+ nil
150
+ end
151
+
152
+ # sidekiq-scheduler skips a job whose rails_env doesn't list this one.
153
+ def scheduled_in_this_environment?(job)
154
+ job["rails_env"].nil? || job["rails_env"].to_s.gsub(/\s/, "").split(",").include?(::Rails.env.to_s)
155
+ end
156
+
157
+ # The zone sidekiq-cron and sidekiq-scheduler read a schedule in when it
158
+ # names none: the process's local zone, as Fugit and rufus-scheduler
159
+ # find it. nil if that can't be told.
160
+ def local_time_zone
161
+ ::EtOrbi.determine_local_tzone&.name if defined?(::EtOrbi)
162
+ rescue StandardError
163
+ nil
164
+ end
165
+
166
+ # The path, or the same name with .yaml if only that exists.
167
+ def yaml_path(path)
168
+ [ path, path.sub(/\.yml\z/, ".yaml") ].find { |candidate| File.file?(candidate) }
169
+ end
170
+
171
+ def load_yaml(path)
172
+ YAML.safe_load(ERB.new(File.read(path), trim_mode: "-").result, permitted_classes: [ Symbol ], aliases: true)
173
+ end
174
+
175
+ # Sidekiq's config uses symbol keys (:scheduler:); compare them as
176
+ # plain strings.
177
+ def normalize_keys(object)
178
+ case object
179
+ when Hash then object.to_h { |key, value| [ key.to_s.delete_prefix(":"), normalize_keys(value) ] }
180
+ when Array then object.map { |value| normalize_keys(value) }
181
+ else object
182
+ end
183
+ end
184
+
80
185
  # A command task runs as a job of this class, so its runs show up in
81
186
  # job telemetry under that name.
82
187
  def command_job_class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deployangel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens