cronwatch 0.14.1 → 0.14.3
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/lib/cronwatch/alerts/slack.rb +10 -2
- data/lib/cronwatch/client.rb +63 -14
- data/lib/cronwatch/evaluate.rb +10 -1
- data/lib/cronwatch/http.rb +25 -10
- data/lib/cronwatch/job.rb +18 -6
- data/lib/cronwatch/pg_cron.rb +7 -1
- data/lib/cronwatch/version.rb +1 -1
- data/lib/cronwatch/web/app.rb +17 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d78436fcdc58bdb265650a95d09bfc6300133aa0d24434cfb575fe3f726bb846
|
|
4
|
+
data.tar.gz: 1c52f21a7b4a2c724c8cfa75932e38b56eade9198f53602718c4d5be78eebe24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 554b274bd8e3058755b2459778d4addcc6de0d2ab39fef01d47c15837d3dee796837333df8c019eaba223b63505cfe6f7c9ef331d0d0fd8d8bcae0ed1c5d46c1
|
|
7
|
+
data.tar.gz: d199b491b1260aea57e40852d36827455c69f3aec205fec3b33b0df631b432dcabce27e75b5a3d605e72f1c3e63531af2620e71e50726e524467f63664e4cdd3
|
|
@@ -29,14 +29,14 @@ module Cronwatch
|
|
|
29
29
|
def call(alert)
|
|
30
30
|
url = @link&.call(alert)
|
|
31
31
|
title = "#{EMOJI[alert.type]} *#{Slack.escape(alert.title)}*#{url && !url.empty? ? " (<#{Slack.link_target(url)}|open>)" : ""}"
|
|
32
|
-
body =
|
|
32
|
+
body = Slack.cut_escaped(Slack.code_block_safe(Slack.escape(alert.message)), 2900)
|
|
33
33
|
blocks = [
|
|
34
34
|
{ "type" => "section", "text" => { "type" => "mrkdwn", "text" => title } },
|
|
35
35
|
{ "type" => "section", "text" => { "type" => "mrkdwn", "text" => "```#{body}```" } },
|
|
36
36
|
]
|
|
37
37
|
if alert.triage && !alert.triage.empty?
|
|
38
38
|
# Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.
|
|
39
|
-
blocks << { "type" => "section", "text" => { "type" => "mrkdwn", "text" =>
|
|
39
|
+
blocks << { "type" => "section", "text" => { "type" => "mrkdwn", "text" => Slack.cut_escaped("_Triage:_ #{Slack.escape(alert.triage)}", 3000) } }
|
|
40
40
|
end
|
|
41
41
|
payload = {
|
|
42
42
|
# The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.
|
|
@@ -52,6 +52,14 @@ module Cronwatch
|
|
|
52
52
|
text.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Escaped text cut to at most `max` UTF-16 units, splitting neither an
|
|
56
|
+
# entity escape wrote nor a surrogate pair.
|
|
57
|
+
def self.cut_escaped(text, max)
|
|
58
|
+
kept = JS.head16(text, max)
|
|
59
|
+
amp = kept.rindex("&")
|
|
60
|
+
amp && !kept.index(";", amp) ? kept[0, amp] : kept
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
# A link's URL as Slack's <url|text> can hold it: `<`, `>`, and `|`
|
|
56
64
|
# percent-encoded, which ends neither the link nor its text.
|
|
57
65
|
def self.link_target(url)
|
data/lib/cronwatch/client.rb
CHANGED
|
@@ -45,9 +45,13 @@ module Cronwatch
|
|
|
45
45
|
# The longest run id, in UTF-16 code units (JavaScript's string length):
|
|
46
46
|
# what start, resume, and record_run take, and every store holds.
|
|
47
47
|
MAX_RUN_ID = 200
|
|
48
|
+
# The statuses record_run takes.
|
|
49
|
+
RUN_STATUSES = %w[running ok failed timeout].freeze
|
|
50
|
+
# The error a check gives an import whose finish never landed (Evaluate.stranded?).
|
|
51
|
+
STRANDED_ERROR = "Imported, but its finish was never written; marked as timed out"
|
|
48
52
|
private_constant :CHANNEL_TIMEOUT_MS, :DEFAULT_OPTIONS, :FORK_LOCK, :HISTORY_MAX, :HISTORY_PAGE, :MAX_RUN_ID,
|
|
49
|
-
:PRUNE_INTERVAL_MS, :RESERVED_RUN_ID_PREFIX, :RETRY_BUDGET_MS, :
|
|
50
|
-
:TRIAGE_TIMEOUT_MS
|
|
53
|
+
:PRUNE_INTERVAL_MS, :RESERVED_RUN_ID_PREFIX, :RETRY_BUDGET_MS, :RUN_STATUSES, :SILENCE_OPTIONS,
|
|
54
|
+
:STATE_ATTEMPTS, :STRANDED_ERROR, :TRIAGE_TIMEOUT_MS
|
|
51
55
|
|
|
52
56
|
# What execute returns: the recorded run, and the block's own outcome.
|
|
53
57
|
#
|
|
@@ -362,7 +366,8 @@ module Cronwatch
|
|
|
362
366
|
# redacted the same way; one finishing after a check marked it timeout is
|
|
363
367
|
# judged only when it succeeded, as RunHandle#finish does. A metric that
|
|
364
368
|
# is not a finite number raises ArgumentError before anything is written,
|
|
365
|
-
# as JobContext#metric does
|
|
369
|
+
# as JobContext#metric does, and so does a status, start, finish,
|
|
370
|
+
# duration, error, output, or trigger evaluation cannot read. `evaluate: false` stores it without judging
|
|
366
371
|
# it, for history imported on first sight. Returns the alerts it sent.
|
|
367
372
|
#
|
|
368
373
|
# `run` is a Cronwatch::Run, or a hash of its fields (camelCase or snake_case keys).
|
|
@@ -379,6 +384,7 @@ module Cronwatch
|
|
|
379
384
|
if input.id.include?("\0")
|
|
380
385
|
raise ArgumentError, "record_run: run ids cannot contain a NUL character (job \"#{input.job}\")"
|
|
381
386
|
end
|
|
387
|
+
check_record_fields(input)
|
|
382
388
|
# Refused as JobContext#metric refuses them: a store keeps NaN and Infinity as null.
|
|
383
389
|
unless input.metrics.nil? || input.metrics.is_a?(Hash)
|
|
384
390
|
raise ArgumentError, "record_run: metrics must be a Hash of numbers (job \"#{input.job}\", run \"#{input.id}\")"
|
|
@@ -410,8 +416,18 @@ module Cronwatch
|
|
|
410
416
|
stored = @store.get_run(run.id)
|
|
411
417
|
return record_over(definition, stored, run, evaluate) if stored
|
|
412
418
|
|
|
419
|
+
# An evaluated import stays retryable until its durable evaluation marker
|
|
420
|
+
# is written. A finished one's placeholder has finished_at set to when it
|
|
421
|
+
# was written, so a check can tell one whose finish never landed
|
|
422
|
+
# (Evaluate.stranded?); the finish writes the run's own. One still
|
|
423
|
+
# running keeps finished_at nil, so it times out as any running run does.
|
|
424
|
+
inserted = run.dup
|
|
425
|
+
if evaluate
|
|
426
|
+
inserted.finished_at = now unless inserted.status == :running
|
|
427
|
+
inserted.status = :running
|
|
428
|
+
end
|
|
413
429
|
begin
|
|
414
|
-
@store.insert_run(
|
|
430
|
+
@store.insert_run(inserted)
|
|
415
431
|
rescue StandardError
|
|
416
432
|
# Another process recorded it first.
|
|
417
433
|
again = begin
|
|
@@ -425,10 +441,7 @@ module Cronwatch
|
|
|
425
441
|
end
|
|
426
442
|
return [] unless evaluate
|
|
427
443
|
|
|
428
|
-
|
|
429
|
-
return [] if run.status == :running
|
|
430
|
-
|
|
431
|
-
finish_run(definition, run, now, pending_only: true)
|
|
444
|
+
record_over(definition, inserted, run, true)
|
|
432
445
|
end
|
|
433
446
|
|
|
434
447
|
# Hands an error to on_error, as a source reports what went wrong. See #report.
|
|
@@ -911,13 +924,42 @@ module Cronwatch
|
|
|
911
924
|
end
|
|
912
925
|
end
|
|
913
926
|
|
|
927
|
+
# The fields evaluation reads, checked before record_run writes anything.
|
|
928
|
+
def check_record_fields(input)
|
|
929
|
+
where = "job \"#{input.job}\", run \"#{input.id}\""
|
|
930
|
+
status = input.status
|
|
931
|
+
unless (status.is_a?(Symbol) || status.is_a?(String)) && RUN_STATUSES.include?(status.to_s)
|
|
932
|
+
raise ArgumentError, "record_run: status must be \"running\", \"ok\", \"failed\", or \"timeout\" (#{where})"
|
|
933
|
+
end
|
|
934
|
+
raise ArgumentError, "record_run: started_at must be a finite number (#{where})" unless JS.finite?(input.started_at)
|
|
935
|
+
raise ArgumentError, "record_run: finished_at must be a finite number or nil (#{where})" unless input.finished_at.nil? || JS.finite?(input.finished_at)
|
|
936
|
+
raise ArgumentError, "record_run: duration_ms must be a finite number or nil (#{where})" unless input.duration_ms.nil? || JS.finite?(input.duration_ms)
|
|
937
|
+
|
|
938
|
+
{ "error" => input.error, "output" => input.output }.each do |field, value|
|
|
939
|
+
raise ArgumentError, "record_run: #{field} must be a String or nil (#{where})" unless value.nil? || value.is_a?(String)
|
|
940
|
+
end
|
|
941
|
+
raise ArgumentError, "record_run: trigger must be a String (#{where})" unless input.trigger.is_a?(String)
|
|
942
|
+
end
|
|
943
|
+
|
|
914
944
|
# record_run for a run already stored.
|
|
915
945
|
def record_over(definition, stored, run, evaluate)
|
|
916
946
|
if stored.job != run.job
|
|
917
947
|
report(RuntimeError.new("run #{run.id} of #{run.job} belongs to job \"#{stored.job}\"; ignored"), "recording #{run.job}")
|
|
918
948
|
return []
|
|
919
949
|
end
|
|
920
|
-
return []
|
|
950
|
+
return [] unless %i[running timeout].include?(stored.status)
|
|
951
|
+
|
|
952
|
+
if evaluate && stored.status == :running
|
|
953
|
+
update_state(run.job) do |before|
|
|
954
|
+
# Recheck each CAS attempt so a concurrent finish cannot resurrect its marker.
|
|
955
|
+
current = @store.get_run(run.id)
|
|
956
|
+
if current.nil? || current.job != run.job || current.status != :running || (before.pending_runs || []).include?(run.id)
|
|
957
|
+
next [before, nil]
|
|
958
|
+
end
|
|
959
|
+
[Evaluate.hold_run(Evaluate.on_run_start(definition, run, before, now), run.id), nil]
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
return [] if run.status == :running
|
|
921
963
|
|
|
922
964
|
tracked = (read_state(run.job).pending_runs || []).include?(run.id)
|
|
923
965
|
late, ignored = claim_finish(run)
|
|
@@ -1337,18 +1379,25 @@ module Cronwatch
|
|
|
1337
1379
|
next if definition.nil?
|
|
1338
1380
|
|
|
1339
1381
|
evaluable(listed.job, definition)
|
|
1340
|
-
|
|
1382
|
+
stranded = Evaluate.stranded?(listed, at)
|
|
1383
|
+
next unless stranded || Evaluate.stuck?(definition, listed, at)
|
|
1341
1384
|
|
|
1342
1385
|
# Read again just before the write: lines and metrics flushed since the
|
|
1343
1386
|
# list was read (while earlier stuck runs were sent, say) are kept.
|
|
1344
1387
|
run = @store.get_run(listed.id)
|
|
1345
1388
|
next if run.nil? || run.status != :running || run.job != listed.job
|
|
1346
1389
|
|
|
1347
|
-
timeout = Evaluate.timeout_ms(definition)
|
|
1348
1390
|
run.status = :timeout
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1391
|
+
if stranded
|
|
1392
|
+
# Its own status was never written; a late finish of it is judged
|
|
1393
|
+
# as one after a timeout, so a success still recovers.
|
|
1394
|
+
run.duration_ms = Evaluate.run_duration(run.started_at, run.finished_at) if run.duration_ms.nil?
|
|
1395
|
+
run.error = run.error.nil? ? STRANDED_ERROR : "#{STRANDED_ERROR}\n#{run.error}"
|
|
1396
|
+
else
|
|
1397
|
+
run.finished_at = at
|
|
1398
|
+
run.duration_ms = Evaluate.run_duration(run.started_at, at)
|
|
1399
|
+
run.error = "Still running after #{Duration.format(Evaluate.timeout_ms(definition))}; marked as timed out"
|
|
1400
|
+
end
|
|
1352
1401
|
# Only over a row still running: a finish that landed meanwhile wins.
|
|
1353
1402
|
next unless write_run_if(run, [:running])
|
|
1354
1403
|
|
data/lib/cronwatch/evaluate.rb
CHANGED
|
@@ -688,7 +688,16 @@ module Cronwatch
|
|
|
688
688
|
|
|
689
689
|
# Whether a running run has gone on longer than the job's timeout.
|
|
690
690
|
def stuck?(definition, run, now)
|
|
691
|
-
run.status == :running && now - run.started_at > timeout_ms(definition)
|
|
691
|
+
run.status == :running && run.finished_at.nil? && now - run.started_at > timeout_ms(definition)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Whether a finished import was left as its placeholder: record_run writes
|
|
695
|
+
# one as running, with finished_at set to when it was written, and the
|
|
696
|
+
# finish that follows never landed (the importer stopped, or a store write
|
|
697
|
+
# failed and it was not retried). A check closes one SEND_LEASE_MS after
|
|
698
|
+
# it was written, as it closes a stuck run.
|
|
699
|
+
def stranded?(run, now)
|
|
700
|
+
run.status == :running && !run.finished_at.nil? && now - run.finished_at > SEND_LEASE_MS
|
|
692
701
|
end
|
|
693
702
|
|
|
694
703
|
# While a job is silenced nothing new is recorded as an incident: conditions
|
data/lib/cronwatch/http.rb
CHANGED
|
@@ -55,25 +55,41 @@ module Cronwatch
|
|
|
55
55
|
# name lookup: it can be interrupted from Ruby 3.3. On Ruby 3.2 a lookup
|
|
56
56
|
# that hangs is cut off only by the client's channel deadline.
|
|
57
57
|
http.open_timeout = HTTP.remaining(deadline)
|
|
58
|
-
|
|
58
|
+
# Net::HTTP's read and write timeouts restart for each socket operation.
|
|
59
|
+
# Bound the entire exchange too, including a status line or headers
|
|
60
|
+
# arriving one byte at a time. The connection is private to this call.
|
|
61
|
+
answer = nil
|
|
62
|
+
Timeout.timeout(HTTP.remaining(deadline), TimeoutError, "The operation was aborted due to timeout") do
|
|
63
|
+
http.start
|
|
59
64
|
remaining = HTTP.remaining(deadline)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
http.read_timeout = remaining
|
|
66
|
+
http.write_timeout = remaining
|
|
67
|
+
http.request(request) do |response|
|
|
63
68
|
status = response.code.to_i
|
|
69
|
+
answer = Response.new(status: status, body: "".b)
|
|
64
70
|
# Returning from inside the block leaves the rest unread; the connection closes after.
|
|
65
|
-
return
|
|
71
|
+
return answer if status >= 200 && status < 300
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
read_body(http, response, deadline, answer.body)
|
|
74
|
+
return answer
|
|
68
75
|
end
|
|
69
76
|
end
|
|
70
|
-
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
|
|
77
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, TimeoutError
|
|
78
|
+
return answer if answer
|
|
79
|
+
|
|
71
80
|
raise TimeoutError
|
|
81
|
+
ensure
|
|
82
|
+
# Timeout has cancelled its timer before this cleanup runs. A timeout
|
|
83
|
+
# during close must not replace an answer or interrupt socket cleanup.
|
|
84
|
+
http.finish if http&.started?
|
|
72
85
|
end
|
|
73
86
|
|
|
74
87
|
# A connection that gives up after ten seconds, as the SDK's requests do.
|
|
88
|
+
# It goes direct: nil as the proxy address keeps Net::HTTP from taking
|
|
89
|
+
# one from http_proxy or https_proxy, which fetch never reads, so an
|
|
90
|
+
# alert (and the credentials in it) reaches only the channel's own host.
|
|
75
91
|
def connection(uri)
|
|
76
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port, nil)
|
|
77
93
|
http.use_ssl = uri.scheme == "https"
|
|
78
94
|
http.open_timeout = @timeout
|
|
79
95
|
http.read_timeout = @timeout
|
|
@@ -85,8 +101,7 @@ module Cronwatch
|
|
|
85
101
|
|
|
86
102
|
# The body, read in chunks until it ends or MAX_BODY bytes have come,
|
|
87
103
|
# whichever is first; what had come when the deadline passes first.
|
|
88
|
-
def read_body(conn, response, deadline)
|
|
89
|
-
body = "".b
|
|
104
|
+
def read_body(conn, response, deadline, body)
|
|
90
105
|
conn.read_timeout = HTTP.remaining(deadline)
|
|
91
106
|
response.read_body do |chunk|
|
|
92
107
|
raise TimeoutError if HTTP.monotonic >= deadline
|
data/lib/cronwatch/job.rb
CHANGED
|
@@ -111,8 +111,14 @@ module Cronwatch
|
|
|
111
111
|
# dropped from @lines: what expect_text needs once the output runs long.
|
|
112
112
|
@head = []
|
|
113
113
|
@head_size = 0
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
# Every line logged while they come to no more than twice the cap
|
|
115
|
+
# (nil after), joined only when read, so a log is not a copy of them all.
|
|
116
|
+
@whole = []
|
|
117
|
+
@whole_size = 0
|
|
118
|
+
# The raw lines behind expect_text's tail, each with its UTF-16 length,
|
|
119
|
+
# joined only when it is read.
|
|
120
|
+
@raw = []
|
|
121
|
+
@raw_size = 0
|
|
116
122
|
@logged = false
|
|
117
123
|
@metrics = {}
|
|
118
124
|
@lock = Mutex.new
|
|
@@ -126,9 +132,15 @@ module Cronwatch
|
|
|
126
132
|
separator = @logged ? "\n" : ""
|
|
127
133
|
@logged = true
|
|
128
134
|
if @whole
|
|
129
|
-
@
|
|
135
|
+
@whole_size += separator.length + length
|
|
136
|
+
@whole = @whole_size <= 2 * Output::CAP ? @whole << line.dup : nil
|
|
130
137
|
end
|
|
131
|
-
|
|
138
|
+
raw = length > Output::CAP ? JS.tail16(line, Output::CAP).dup : line.dup
|
|
139
|
+
raw_length = length > Output::CAP ? JS.length16(raw) : length
|
|
140
|
+
@raw << [raw, raw_length]
|
|
141
|
+
@raw_size += raw_length + 1
|
|
142
|
+
# Drops the oldest only while what is left still holds the cap.
|
|
143
|
+
@raw_size -= @raw.shift[1] + 1 while @raw.length > 1 && @raw_size - (@raw[0][1] + 1) > Output::CAP
|
|
132
144
|
if @head_size < Output::CAP
|
|
133
145
|
@head << JS.head16(line, Output::CAP - @head_size).dup
|
|
134
146
|
@head_size += length + 1
|
|
@@ -165,9 +177,9 @@ module Cronwatch
|
|
|
165
177
|
@lock.synchronize do
|
|
166
178
|
next nil if @lines.empty?
|
|
167
179
|
|
|
168
|
-
next @whole if @whole
|
|
180
|
+
next @whole.join("\n") if @whole
|
|
169
181
|
|
|
170
|
-
"#{JS.head16(@head.join("\n"), Output::CAP)}\n#{@
|
|
182
|
+
"#{JS.head16(@head.join("\n"), Output::CAP)}\n#{JS.tail16(@raw.map(&:first).join("\n"), Output::CAP)}"
|
|
171
183
|
end
|
|
172
184
|
end
|
|
173
185
|
|
data/lib/cronwatch/pg_cron.rb
CHANGED
|
@@ -109,6 +109,7 @@ module Cronwatch
|
|
|
109
109
|
@pending = {}
|
|
110
110
|
# Runs read before they started, by runid, with when they were first seen.
|
|
111
111
|
@held = {}
|
|
112
|
+
@retry = {}
|
|
112
113
|
# Each job's name and definition as last declared, by jobid.
|
|
113
114
|
@known = {}
|
|
114
115
|
# The last definition declared for each name, so an unchanged job is not declared again.
|
|
@@ -493,10 +494,12 @@ module Cronwatch
|
|
|
493
494
|
def record(host, names, row, evaluate, alerts, now)
|
|
494
495
|
runid = Integer(row["runid"])
|
|
495
496
|
jobid = Integer(row["jobid"])
|
|
497
|
+
evaluate = @retry.fetch(runid, evaluate)
|
|
496
498
|
name = @pending[runid] || names[jobid]
|
|
497
499
|
if name.nil? || (@declared_now && !names.value?(name) && !@declared_now.include?(name))
|
|
498
500
|
@pending.delete(runid)
|
|
499
501
|
@held.delete(runid)
|
|
502
|
+
@retry.delete(runid)
|
|
500
503
|
@retired.delete(name) if name
|
|
501
504
|
return
|
|
502
505
|
end
|
|
@@ -516,9 +519,11 @@ module Cronwatch
|
|
|
516
519
|
begin
|
|
517
520
|
alerts.concat(host.record_run(run, evaluate: evaluate))
|
|
518
521
|
rescue StandardError => e
|
|
522
|
+
@retry[runid] = evaluate
|
|
519
523
|
host.on_error(e, "source pg_cron: run #{runid}")
|
|
520
524
|
return
|
|
521
525
|
end
|
|
526
|
+
@retry.delete(runid)
|
|
522
527
|
if run.status == :running
|
|
523
528
|
@pending[runid] = name
|
|
524
529
|
else
|
|
@@ -561,7 +566,7 @@ module Cronwatch
|
|
|
561
566
|
id = run_id_of(run.id)
|
|
562
567
|
@pending[id] = run.job if id && watched.include?(run.job)
|
|
563
568
|
end
|
|
564
|
-
open = (@pending.keys + @held.keys).to_set
|
|
569
|
+
open = (@pending.keys + @held.keys + @retry.keys).to_set
|
|
565
570
|
complete = false
|
|
566
571
|
MAX_PAGES.times do
|
|
567
572
|
jobids = names.keys
|
|
@@ -585,6 +590,7 @@ module Cronwatch
|
|
|
585
590
|
open.each do |runid|
|
|
586
591
|
@pending.delete(runid)
|
|
587
592
|
@held.delete(runid)
|
|
593
|
+
@retry.delete(runid)
|
|
588
594
|
end
|
|
589
595
|
end
|
|
590
596
|
end
|
data/lib/cronwatch/version.rb
CHANGED
data/lib/cronwatch/web/app.rb
CHANGED
|
@@ -204,6 +204,14 @@ module Cronwatch
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
def call(env)
|
|
207
|
+
status, headers, body = answer(env)
|
|
208
|
+
# A HEAD reads what a GET would, and is answered without the body.
|
|
209
|
+
env["REQUEST_METHOD"].to_s.upcase == "HEAD" ? [status, headers, []] : [status, headers, body]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def answer(env)
|
|
207
215
|
wants_html = true
|
|
208
216
|
base = @base_path || env["SCRIPT_NAME"].to_s.sub(%r{/+\z}, "")
|
|
209
217
|
begin
|
|
@@ -228,8 +236,6 @@ module Cronwatch
|
|
|
228
236
|
end
|
|
229
237
|
end
|
|
230
238
|
|
|
231
|
-
private
|
|
232
|
-
|
|
233
239
|
def serve(request, path, wants_html, base)
|
|
234
240
|
cw = client
|
|
235
241
|
method = request.verb
|
|
@@ -308,14 +314,15 @@ module Cronwatch
|
|
|
308
314
|
return wants_html ? html(HTML.message_page("Bad request", "The path is not valid.", base), 400) : api({ ok: false, error: "Bad path" }, 400)
|
|
309
315
|
end
|
|
310
316
|
|
|
311
|
-
# HTML
|
|
312
|
-
|
|
317
|
+
# HTML. A HEAD reads what a GET would; call sends it without the body.
|
|
318
|
+
reads = %w[GET HEAD].include?(method)
|
|
319
|
+
if reads && path == "/"
|
|
313
320
|
entries = cw.jobs_with_runs(BOARD_PAGE_RUNS)
|
|
314
321
|
now = cw.now
|
|
315
322
|
runs_by_job = entries.to_h { |entry| [entry.job.name, entry.runs] }
|
|
316
323
|
return html(HTML.dashboard_page(entries.map(&:job), runs_by_job, now, base, nil, board_lanes(cw, entries, now)))
|
|
317
324
|
end
|
|
318
|
-
if
|
|
325
|
+
if reads && parts[0] == "jobs" && parts.length == 2
|
|
319
326
|
job = cw.job_summary(parts[1])
|
|
320
327
|
return html(HTML.message_page("No such job", "#{parts[1]} is not in the store.", base), 404) unless job
|
|
321
328
|
|
|
@@ -363,14 +370,15 @@ module Cronwatch
|
|
|
363
370
|
|
|
364
371
|
def serve_api(request, method, rest, bearer)
|
|
365
372
|
cw = client
|
|
366
|
-
|
|
373
|
+
reads = %w[GET HEAD].include?(method)
|
|
374
|
+
if reads && rest.empty?
|
|
367
375
|
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION })
|
|
368
376
|
end
|
|
369
|
-
return api({ ok: true, jobs: cw.jobs }) if
|
|
377
|
+
return api({ ok: true, jobs: cw.jobs }) if reads && rest[0] == "jobs" && rest.length == 1
|
|
370
378
|
|
|
371
379
|
if rest[0] == "jobs" && rest.length == 2
|
|
372
380
|
name = rest[1]
|
|
373
|
-
if
|
|
381
|
+
if reads
|
|
374
382
|
job = cw.job_summary(name)
|
|
375
383
|
return api({ ok: false, error: "No such job" }, 404) unless job
|
|
376
384
|
|
|
@@ -413,7 +421,7 @@ module Cronwatch
|
|
|
413
421
|
end
|
|
414
422
|
return api({ "ok" => true }.merge(cw.check.to_h)) if %w[GET POST].include?(method)
|
|
415
423
|
end
|
|
416
|
-
if
|
|
424
|
+
if reads && rest[0] == "runs" && rest.length == 2
|
|
417
425
|
run = cw.get_run(rest[1])
|
|
418
426
|
return run ? api({ ok: true, run: run }) : api({ ok: false, error: "No such run" }, 404)
|
|
419
427
|
end
|