cronwatch 0.8.0 → 0.10.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: b8af9eac18f781dcf622c274e6f3e44370a4373d0721215ba2928f133d3b36bd
4
- data.tar.gz: 20577e3909253de5b132c4fba48654b9176a593efc8c5c86caf85845fe16c1c7
3
+ metadata.gz: cb5e3c0550792cfd9ba9d34f828cf099051f30b5b52cfd780373efe581e665e2
4
+ data.tar.gz: ba21834f9a3d3b8b641e26fa23990f34f12f2190b7ad5bbf7d9355b0bdc0685f
5
5
  SHA512:
6
- metadata.gz: 3f29e87d9cd6b09776a1c744c147b29ac20f6311072e2f7eb7d16b5ba4eaf4f29b3cbb204567badb4f655af12224fb308923a7e6b1f8e4189fd9b42db2df2079
7
- data.tar.gz: 8c3e6d1326f545a5fa453235a385814351c0575927a024ed0fc341cb6a2e639e996856ec4ca96eb2f4e674b0d90a70789a431514b9c4e7f140cb917efd12de0d
6
+ metadata.gz: bf853e3e1f3ac6db1f74178033a1bd92a0863a182285bfe944e872a8463f9a261319b8c5c27d5c2a2acd31d54a4f328a22b6a80d1d722170414aef18785e2dc4
7
+ data.tar.gz: f23c2439e7b9d0fb9f9f818cc49313c15d0e35e42dcffb74aaf246a327d4318b8951293060e269cf1e01224f5986ac25090ce9ef92c4e688f0d4e7feb8614976
data/README.md CHANGED
@@ -74,7 +74,7 @@ bin/rails generate cronwatch:install # a migration and config/initializers/cr
74
74
  bin/rails db:migrate
75
75
  ```
76
76
 
77
- The generator takes `--prefix` (table prefix, default `cronwatch_`) and `--database` (the database whose migrations directory gets the migration). The initializer it writes sets the ActiveRecord store, which Rails needs so web, worker and check processes see the same runs:
77
+ The generator takes `--prefix` (table prefix, default `cronwatch_`) and `--database` (the database, from `config/database.yml`, whose migrations directory gets the migration; it also writes `app/models/cronwatch_record.rb`, an abstract class that `connects_to` that database, and names it as the store's `connection_class`, so the store reads the tables where the migration made them). The initializer it writes sets the ActiveRecord store, which Rails needs so web, worker and check processes see the same runs:
78
78
 
79
79
  ```ruby
80
80
  # config/initializers/cronwatch.rb
@@ -124,7 +124,7 @@ The same `cronwatch` as for ActiveJob, for classes that include `Sidekiq::Job` (
124
124
  cronwatch schedule: :from_scheduler, grace: "15m" # ActiveJob or Sidekiq
125
125
  ```
126
126
 
127
- reads the class's one entry in Solid Queue's `config/recurring.yml` (the section for `Rails.env`) or sidekiq-cron's `config/schedule.yml`, so the schedule is written once. Fugit phrases such as `every day at 3am` or `every hour at minute 12` become the cron expression Fugit makes of them (`0 3 * * *`, `12 * * * *`), in the zone the scheduler reads them in. Each conversion is checked against Fugit's own next run times; anything CronWatch would not expect at exactly those times is refused at boot rather than approximated: every other week (`%`), days counted back from the month's end other than the last, random times (`~`), offsets instead of IANA zones, and a time that daylight saving skips (Fugit skips that run; CronWatch would report it missed). A class with no entry, or with two, also stops the boot.
127
+ reads the class's one entry in Solid Queue's `config/recurring.yml` (the section for `Rails.env`) or sidekiq-cron's `config/schedule.yml`, so the schedule is written once. Fugit phrases such as `every day at 3am` or `every hour at minute 12` become the cron expression Fugit makes of them (`0 3 * * *`, `12 * * * *`), in the zone the scheduler reads them in. Each conversion is checked against Fugit's own next run times; anything CronWatch would not expect at exactly those times is refused at boot rather than approximated: every other week (`%`), days counted back from the month's end other than the last, random times (`~`), offsets instead of IANA zones, and a time that daylight saving skips (Fugit skips that run; CronWatch would report it missed). A class with no entry, or with two, also stops the boot; one scheduled only in another environment's section (Rails 8's `recurring.yml` has only `production:`) is declared without a schedule, and a process started with `SOLID_QUEUE_SKIP_RECURRING` still reads the entry.
128
128
 
129
129
  `Cronwatch.declare_from_scheduler!(grace: "10m")` in the initializer watches every enabled entry once the app has booted: classes that call `cronwatch` declare themselves, other classes are named as `cronwatch` would name them and their runs are recorded, and Solid Queue `command:` tasks are named after their key. `except:` leaves keys out. `Cronwatch::Scheduler.sources` sets where to read, when the defaults (Solid Queue's file when Solid Queue is loaded, sidekiq-cron's when sidekiq-cron is) are not right.
130
130
 
@@ -168,7 +168,7 @@ Rails.application.routes.draw do
168
168
  end
169
169
  ```
170
170
 
171
- Set `CRONWATCH_TOKEN` to a long random string and open `/cronwatch?token=<it>` once; the browser keeps a cookie. Without a token, while the environment (`Rails.env`, else `RAILS_ENV` or `RACK_ENV`) is `development` or `test`, it makes a token of its own and prints a sign-in link to standard output on its first request (open it once; unless `origin:` is set or the request's host is loopback, the link leaves the host out, since a client chooses it); anywhere else it answers 503. To rely on the app's own sign in, mount it behind that (Devise's `authenticate` block, or a routing constraint) and pass `token: nil`. The URLs, JSON shapes, headers and CSRF rules are the SDK's, so the MCP server reads it unchanged. The request's origin, which the CSRF check compares against, is what Rack reports and so already follows `X-Forwarded-Host` and `X-Forwarded-Proto`; pass `origin: "https://app.example.com"` to pin it instead. `GET /cronwatch/api/check` with a bearer (the token or `CRON_SECRET`) runs the check, for an outside cron.
171
+ Set `CRONWATCH_TOKEN` to a long random string and open `/cronwatch?token=<it>` once; the browser keeps a cookie. Without a token, while the environment (`Rails.env`, else `RAILS_ENV` or `RACK_ENV`) is `development` or `test`, it makes a token of its own and prints a sign-in link to standard output on its first request (open it once; unless `origin:` is set or the request's host is loopback, the link leaves the host out, since a client chooses it); anywhere else it answers 503. Puma, Unicorn, Thin and rackup set `RACK_ENV` to `development` when nothing names an environment, production included, so under one of them `RACK_ENV` alone does not count: `Rails.env`, `RAILS_ENV` or `APP_ENV` must say `development` or `test` (or `RACK_ENV` say `test`). To rely on the app's own sign in, mount it behind that (Devise's `authenticate` block, or a routing constraint) and pass `token: nil`. The URLs, JSON shapes, headers and CSRF rules are the SDK's, so the MCP server reads it unchanged. The request's origin, which the CSRF check compares against, is what Rack reports and so already follows `X-Forwarded-Host` and `X-Forwarded-Proto`; pass `origin: "https://app.example.com"` to pin it instead. `GET /cronwatch/api/check` with a bearer (the token or `CRON_SECRET`) runs the check, for an outside cron.
172
172
 
173
173
  There is no `handler()` as in the TypeScript SDK: for a job triggered over HTTP, wrap the controller action's body in `CW.job(...).run` (declared once, at boot) and check the bearer in the controller.
174
174
 
@@ -9,6 +9,10 @@ module Cronwatch
9
9
  missed: 0xb7791f, failed: 0xc62828, stuck: 0xc62828, slow: 0xb7791f, over_budget: 0xb7791f, recovered: 0x1f8a4c,
10
10
  }.freeze
11
11
 
12
+ # The longest embed description Discord takes. The title (under 256)
13
+ # and it stay well inside the embed's 6000.
14
+ DESCRIPTION_MAX = 4096
15
+
12
16
  attr_reader :name
13
17
 
14
18
  def initialize(webhook_url:, link: nil, http: HTTP.default)
@@ -24,8 +28,7 @@ module Cronwatch
24
28
  url = @link&.call(alert)
25
29
  embed = { "title" => alert.title }
26
30
  embed["url"] = url if url && !url.empty?
27
- triage = alert.triage && !alert.triage.empty? ? "\n**Triage:** #{Discord.escape_markdown(JS.head16(alert.triage, 1000))}" : ""
28
- embed["description"] = "```\n#{Discord.code_block_safe(JS.head16(alert.message, 3800))}\n```#{triage}"
31
+ embed["description"] = Discord.embed_description(alert)
29
32
  embed["color"] = COLOR[alert.type]
30
33
  embed["timestamp"] = JS.iso(alert.at)
31
34
  payload = {
@@ -38,6 +41,16 @@ module Cronwatch
38
41
  raise "Discord webhook answered #{response.status}: #{JS.head16(response.body.to_s, 200)}" unless response.ok?
39
42
  end
40
43
 
44
+ # The message in a code block, then the triage. Each part has its own
45
+ # cap, and escaping can grow both, so the whole is held to
46
+ # DESCRIPTION_MAX (in UTF-16 units) by cutting the message's block,
47
+ # never the triage: Discord refuses a longer one on every retry.
48
+ def self.embed_description(alert)
49
+ triage = alert.triage && !alert.triage.empty? ? "\n**Triage:** #{escape_markdown(JS.head16(alert.triage, 1000))}" : ""
50
+ room = DESCRIPTION_MAX - "```\n".length - "\n```".length - JS.length16(triage)
51
+ "```\n#{JS.head16(code_block_safe(JS.head16(alert.message, 3800)), room)}\n```#{triage}"
52
+ end
53
+
41
54
  # Breaks up ``` so text inside a code block cannot close it.
42
55
  def self.code_block_safe(text)
43
56
  text.gsub("```", "`\u200b`\u200b`")