cronwatch 0.9.0 → 0.11.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 +4 -4
- data/README.md +20 -11
- data/lib/cronwatch/alerts/datadog.rb +1 -0
- data/lib/cronwatch/alerts/discord.rb +34 -2
- data/lib/cronwatch/alerts/email.rb +2 -0
- data/lib/cronwatch/alerts/honeybadger.rb +1 -0
- data/lib/cronwatch/alerts/postmark.rb +1 -0
- data/lib/cronwatch/alerts/provider.rb +2 -0
- data/lib/cronwatch/alerts/resend.rb +1 -0
- data/lib/cronwatch/alerts/rollbar.rb +1 -0
- data/lib/cronwatch/alerts/sentry.rb +1 -0
- data/lib/cronwatch/alerts/sigv4.rb +2 -0
- data/lib/cronwatch/alerts/slack.rb +1 -0
- data/lib/cronwatch/alerts/twilio.rb +1 -0
- data/lib/cronwatch/alerts/webhook.rb +18 -5
- data/lib/cronwatch/client.rb +275 -110
- data/lib/cronwatch/cron_pattern.rb +17 -8
- data/lib/cronwatch/deprecation.rb +18 -0
- data/lib/cronwatch/duration.rb +2 -0
- data/lib/cronwatch/environment.rb +109 -7
- data/lib/cronwatch/evaluate.rb +145 -10
- data/lib/cronwatch/flight.rb +6 -0
- data/lib/cronwatch/format.rb +1 -0
- data/lib/cronwatch/http.rb +21 -5
- data/lib/cronwatch/job.rb +7 -2
- data/lib/cronwatch/js.rb +74 -1
- data/lib/cronwatch/monitored.rb +6 -1
- data/lib/cronwatch/output.rb +28 -1
- data/lib/cronwatch/pg_cron.rb +29 -6
- data/lib/cronwatch/rails/active_job.rb +2 -2
- data/lib/cronwatch/schedule.rb +25 -1
- data/lib/cronwatch/scheduler.rb +79 -11
- data/lib/cronwatch/serialize.rb +1 -0
- data/lib/cronwatch/stats.rb +1 -0
- data/lib/cronwatch/stores/active_record.rb +37 -11
- data/lib/cronwatch/ticker.rb +14 -0
- data/lib/cronwatch/triage/anthropic.rb +2 -0
- data/lib/cronwatch/types.rb +199 -27
- data/lib/cronwatch/version.rb +1 -1
- data/lib/cronwatch/walker.rb +2 -0
- data/lib/cronwatch/web/app.rb +114 -37
- data/lib/cronwatch/web/html.rb +9 -5
- data/lib/cronwatch/web/origin.rb +2 -0
- data/lib/cronwatch/web/pwa.rb +2 -0
- data/lib/cronwatch/web/timeline.rb +2 -0
- data/lib/cronwatch/web.rb +1 -2
- data/lib/cronwatch/zone.rb +2 -0
- data/lib/cronwatch.rb +2 -0
- data/lib/generators/cronwatch/install/install_generator.rb +52 -8
- metadata +2 -1
data/lib/cronwatch/web/html.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Cronwatch
|
|
|
21
21
|
# Motion is CSS only and says something: marks arrive in time order, the
|
|
22
22
|
# now line drops in last, and open problems (a missed slot, a running bar)
|
|
23
23
|
# breathe slowly. prefers-reduced-motion turns all of it off.
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
24
26
|
module HTML
|
|
25
27
|
CSS = "\n" + <<~'CSS'
|
|
26
28
|
:root{color-scheme:light dark;--paper:#f4f4f5;--sheet:#fff;--sunk:#fafafa;--rule:#e4e4e7;--rule-2:#d4d4d8;--tick:#909098;--ink:#000;--body:#18181b;--muted:#71717a;--ok:#15803d;--warn:#a16207;--bad:#b91c1c;--serif:"Newsreader",ui-serif,Georgia,Cambria,"Times New Roman",serif;--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--who:200px}
|
|
@@ -242,7 +244,7 @@ module Cronwatch
|
|
|
242
244
|
|
|
243
245
|
# Object.entries: integer-like keys first, ascending, then the rest in insertion order.
|
|
244
246
|
def entries(hash)
|
|
245
|
-
return []
|
|
247
|
+
return [] unless hash.is_a?(Hash)
|
|
246
248
|
|
|
247
249
|
JS.object_keys(hash).map { |k| [k.to_s, hash[k]] }
|
|
248
250
|
end
|
|
@@ -456,7 +458,8 @@ module Cronwatch
|
|
|
456
458
|
truthy?(run.error) ? %(<details class="out error" open><summary>error</summary><pre>#{h(run.error)}</pre></details>) : "",
|
|
457
459
|
truthy?(run.output) ? %(<details class="out"#{run.status.to_s == "ok" ? "" : " open"}><summary>output</summary><pre>#{h(run.output)}</pre></details>) : "",
|
|
458
460
|
].join
|
|
459
|
-
|
|
461
|
+
# A foreign row may hold a metric that is no finite number (null, text); it is left out.
|
|
462
|
+
metrics = entries(run.metrics).select { |_, v| v.is_a?(Numeric) && v.real? && JS.finite?(v) }.map do |k, v|
|
|
460
463
|
%(<span><span class="k">#{h(k)}</span> #{h(JS.integer?(v) ? v : to_fixed(v, 4))}</span>)
|
|
461
464
|
end.join
|
|
462
465
|
took = run.duration_ms.nil? ? %(<span class="muted">running</span>) : h(Duration.format(run.duration_ms))
|
|
@@ -563,13 +566,14 @@ module Cronwatch
|
|
|
563
566
|
layout("#{job.name}: CronWatch", body, base, refresh: 60)
|
|
564
567
|
end
|
|
565
568
|
|
|
566
|
-
# A page with one message. With `sign_in`, a form under it
|
|
567
|
-
# token
|
|
569
|
+
# A page with one message. With `sign_in`, a form under it posts the
|
|
570
|
+
# token to <base>/signin in the body, keeping it out of the URL and
|
|
571
|
+
# access logs, and the app sets the cookie:
|
|
568
572
|
# the way in where there is no address bar to open a link with, such as
|
|
569
573
|
# an app on an iPhone's home screen, which keeps its cookies apart from
|
|
570
574
|
# Safari's.
|
|
571
575
|
def message_page(title, message, base, sign_in: false)
|
|
572
|
-
form = sign_in ? %(<form class="signin" method="
|
|
576
|
+
form = sign_in ? %(<form class="signin" method="post" action="#{h(base)}/signin"><label for="token">Token</label><input id="token" name="token" type="password" autocomplete="current-password" autocapitalize="off" spellcheck="false" required><button class="primary" type="submit">Sign in</button></form>) : ""
|
|
573
577
|
layout(title, %(<header class="top">#{brand(base)}</header><main class="message"><h1>#{h(title)}</h1><p>#{h(message)}</p>#{form}</main>), base)
|
|
574
578
|
end
|
|
575
579
|
end
|
data/lib/cronwatch/web/origin.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Cronwatch
|
|
|
10
10
|
# ignored, the host is lowercased (percent escapes decoded, IPv4 numbers
|
|
11
11
|
# written out, IPv6 compressed, a non-ASCII host converted to punycode)
|
|
12
12
|
# and a default port is left out. A port outside 1 to 65535 is refused.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
13
15
|
module Origin
|
|
14
16
|
ABSOLUTE = "routes: origin must be an absolute URL such as \"https://app.example.com\", got %s"
|
|
15
17
|
FORBIDDEN_HOST = /[\x00-\x20#%\/:<>?@\[\\\]^|\x7F]/
|
data/lib/cronwatch/web/pwa.rb
CHANGED
|
@@ -7,6 +7,8 @@ module Cronwatch
|
|
|
7
7
|
# that registers it and a page to show offline. None of it says anything
|
|
8
8
|
# about the jobs, so it is served without the token (a browser fetches the
|
|
9
9
|
# manifest and icons without cookies in some flows).
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
10
12
|
module PWA
|
|
11
13
|
# The page colours the app's window takes: the paper behind the sheet,
|
|
12
14
|
# and the sheet the header sits on.
|
data/lib/cronwatch/web.rb
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
# The dashboard and JSON API, as a Rack app. Needs the rack gem; the Rails
|
|
4
4
|
# integration loads this file itself.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# run Cronwatch::Web.new(CW)
|
|
6
|
+
# run CW.routes # Client#routes requires this file itself
|
|
8
7
|
begin
|
|
9
8
|
require "rack"
|
|
10
9
|
rescue LoadError => e
|
data/lib/cronwatch/zone.rb
CHANGED
data/lib/cronwatch.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
require_relative "cronwatch/version"
|
|
6
6
|
require_relative "cronwatch/js"
|
|
7
|
+
require_relative "cronwatch/deprecation"
|
|
7
8
|
require_relative "cronwatch/types"
|
|
8
9
|
require_relative "cronwatch/duration"
|
|
9
10
|
require_relative "cronwatch/stats"
|
|
@@ -49,6 +50,7 @@ module Cronwatch
|
|
|
49
50
|
# The options Cronwatch.configure sets. Anything left unset takes the client's default.
|
|
50
51
|
class Configuration
|
|
51
52
|
OPTIONS = %i[store alerts triage cron_secret retention defaults redact deliver now on_error sources].freeze
|
|
53
|
+
private_constant :OPTIONS
|
|
52
54
|
|
|
53
55
|
attr_accessor(*OPTIONS)
|
|
54
56
|
|
|
@@ -11,8 +11,11 @@ module Cronwatch
|
|
|
11
11
|
# Writes a migration for the three tables the ActiveRecord store uses
|
|
12
12
|
# (created with the SDK's own DDL, so a Node process can share them) and
|
|
13
13
|
# config/initializers/cronwatch.rb, then prints how to schedule
|
|
14
|
-
# Cronwatch::CheckJob and mount the dashboard.
|
|
15
|
-
#
|
|
14
|
+
# Cronwatch::CheckJob and mount the dashboard. With --database, the
|
|
15
|
+
# migration goes to that database and app/models/cronwatch_record.rb, an
|
|
16
|
+
# abstract class connected to it, is the store's connection_class, so the
|
|
17
|
+
# store reads the tables where the migration made them. The templates
|
|
18
|
+
# live in this file so the gem ships nothing but Ruby.
|
|
16
19
|
class InstallGenerator < ::Rails::Generators::Base
|
|
17
20
|
include ::ActiveRecord::Generators::Migration
|
|
18
21
|
|
|
@@ -21,7 +24,10 @@ module Cronwatch
|
|
|
21
24
|
class_option :prefix, type: :string, default: Cronwatch::Stores::ActiveRecord::DEFAULT_PREFIX,
|
|
22
25
|
desc: "Table name prefix: lowercase letters, digits and underscores"
|
|
23
26
|
class_option :database, type: :string, aliases: %i[--db],
|
|
24
|
-
desc: "The database for the
|
|
27
|
+
desc: "The database for the tables, in an app with several (from config/database.yml)"
|
|
28
|
+
|
|
29
|
+
# The abstract class --database writes, which the store connects through.
|
|
30
|
+
RECORD_CLASS = "CronwatchRecord"
|
|
25
31
|
|
|
26
32
|
def check_prefix
|
|
27
33
|
Cronwatch::Stores::ActiveRecord.table_prefix(options[:prefix])
|
|
@@ -41,6 +47,12 @@ module Cronwatch
|
|
|
41
47
|
create_file File.join(dir, "#{number}_#{migration_name}.rb"), migration
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
def create_connection_class
|
|
51
|
+
return unless database_name
|
|
52
|
+
|
|
53
|
+
create_file "app/models/cronwatch_record.rb", connection_class
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def create_initializer
|
|
45
57
|
create_file "config/initializers/cronwatch.rb", initializer
|
|
46
58
|
end
|
|
@@ -60,9 +72,33 @@ module Cronwatch
|
|
|
60
72
|
"create_cronwatch_#{prefix.squeeze("_").delete_prefix("_").delete_suffix("_")}_tables"
|
|
61
73
|
end
|
|
62
74
|
|
|
75
|
+
# The --database name, or nil.
|
|
76
|
+
def database_name
|
|
77
|
+
name = options[:database].to_s
|
|
78
|
+
name.empty? ? nil : name
|
|
79
|
+
end
|
|
80
|
+
|
|
63
81
|
def store_args
|
|
64
82
|
prefix = options[:prefix]
|
|
65
|
-
|
|
83
|
+
args = []
|
|
84
|
+
args << "prefix: #{prefix.inspect}" unless prefix == Cronwatch::Stores::ActiveRecord::DEFAULT_PREFIX
|
|
85
|
+
# Named, not referenced, so the initializer does not load an app class at boot.
|
|
86
|
+
args << "connection_class: #{RECORD_CLASS.inspect}" if database_name
|
|
87
|
+
args.empty? ? "" : "(#{args.join(", ")})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def connection_class
|
|
91
|
+
<<~RUBY
|
|
92
|
+
# frozen_string_literal: true
|
|
93
|
+
|
|
94
|
+
# The #{database_name} database, where CronWatch keeps its tables. The store in
|
|
95
|
+
# config/initializers/cronwatch.rb connects through this class.
|
|
96
|
+
class #{RECORD_CLASS} < ActiveRecord::Base
|
|
97
|
+
self.abstract_class = true
|
|
98
|
+
|
|
99
|
+
connects_to database: { writing: :#{database_name} }
|
|
100
|
+
end
|
|
101
|
+
RUBY
|
|
66
102
|
end
|
|
67
103
|
|
|
68
104
|
def migration
|
|
@@ -102,7 +138,7 @@ module Cronwatch
|
|
|
102
138
|
#
|
|
103
139
|
# and run Cronwatch::CheckJob every few minutes to catch the runs that never happen.
|
|
104
140
|
Cronwatch.configure do |c|
|
|
105
|
-
# Jobs, runs and alert state, in this app's database.
|
|
141
|
+
# Jobs, runs and alert state, in #{database_name ? "the #{database_name} database (see app/models/cronwatch_record.rb)" : "this app's database"}.
|
|
106
142
|
c.store = Cronwatch::Stores::ActiveRecord.new#{store_args}
|
|
107
143
|
|
|
108
144
|
# Where alerts go. With none set, they are written to standard error.
|
|
@@ -127,14 +163,22 @@ module Cronwatch
|
|
|
127
163
|
RUBY
|
|
128
164
|
end
|
|
129
165
|
|
|
166
|
+
# What the next steps say about --database after the migrate line, or "".
|
|
167
|
+
def database_step
|
|
168
|
+
return "" unless database_name
|
|
169
|
+
|
|
170
|
+
"\n\n The store connects through #{RECORD_CLASS} (app/models/cronwatch_record.rb),\n " \
|
|
171
|
+
"which connects_to the #{database_name} database named in config/database.yml."
|
|
172
|
+
end
|
|
173
|
+
|
|
130
174
|
def next_steps
|
|
131
175
|
<<~TEXT
|
|
132
176
|
|
|
133
177
|
CronWatch is installed. Next:
|
|
134
178
|
|
|
135
|
-
1. Create the tables:
|
|
179
|
+
1. Create the tables#{database_name ? " in the #{database_name} database" : ""}:
|
|
136
180
|
|
|
137
|
-
bin/rails db:migrate
|
|
181
|
+
bin/rails db:migrate#{database_step}
|
|
138
182
|
|
|
139
183
|
2. Monitor a job:
|
|
140
184
|
|
|
@@ -163,7 +207,7 @@ module Cronwatch
|
|
|
163
207
|
|
|
164
208
|
4. Mount the dashboard in config/routes.rb:
|
|
165
209
|
|
|
166
|
-
mount Cronwatch
|
|
210
|
+
mount Cronwatch.client.routes => "/cronwatch"
|
|
167
211
|
|
|
168
212
|
Outside development it needs CRONWATCH_TOKEN set to sign in. In
|
|
169
213
|
development, without one, the server prints a sign-in link on
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cronwatch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon C. Phillips
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/cronwatch/alerts/webhook.rb
|
|
58
58
|
- lib/cronwatch/client.rb
|
|
59
59
|
- lib/cronwatch/cron_pattern.rb
|
|
60
|
+
- lib/cronwatch/deprecation.rb
|
|
60
61
|
- lib/cronwatch/duration.rb
|
|
61
62
|
- lib/cronwatch/environment.rb
|
|
62
63
|
- lib/cronwatch/evaluate.rb
|