cronwatch 0.13.1 → 0.14.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 +4 -4
- data/README.md +1 -1
- data/lib/cronwatch/alerts/email.rb +1 -1
- data/lib/cronwatch/alerts/slack.rb +10 -2
- data/lib/cronwatch/alerts/webhook.rb +35 -9
- data/lib/cronwatch/client.rb +76 -38
- data/lib/cronwatch/environment.rb +1 -1
- data/lib/cronwatch/evaluate.rb +174 -31
- data/lib/cronwatch/http.rb +10 -9
- data/lib/cronwatch/job.rb +20 -8
- data/lib/cronwatch/schedule.rb +166 -51
- data/lib/cronwatch/triage/anthropic.rb +3 -3
- data/lib/cronwatch/types.rb +21 -0
- data/lib/cronwatch/version.rb +1 -1
- data/lib/cronwatch/web/app.rb +50 -6
- data/lib/cronwatch/web/timeline.rb +14 -10
- data/lib/cronwatch/zone.rb +3 -0
- metadata +1 -1
data/lib/cronwatch/evaluate.rb
CHANGED
|
@@ -146,15 +146,124 @@ module Cronwatch
|
|
|
146
146
|
"#{alert.type}|#{at.is_a?(Numeric) ? JS.number(at) : at}|#{run.id if run.is_a?(Run)}"
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
#
|
|
149
|
+
# How much of a held alert's run error and output is kept: 2048 UTF-16 units of each.
|
|
150
|
+
HELD_TEXT_CAP = 2048
|
|
151
|
+
|
|
152
|
+
# How far past HELD_TEXT_CAP a text may run and still be kept whole:
|
|
153
|
+
# longer than any marker, so a text trimmed once (its kept part and its
|
|
154
|
+
# marker) is never trimmed again, and the count in its marker stays true.
|
|
155
|
+
HELD_TEXT_SLACK = 64
|
|
156
|
+
|
|
157
|
+
# How much of a held alert's message is kept: its first 4096 UTF-16 units.
|
|
158
|
+
HELD_MESSAGE_CAP = 4096
|
|
159
|
+
|
|
160
|
+
# A run as an alert held in a job's state keeps it (`sending` and
|
|
161
|
+
# `undelivered`), so the state stays small however much a run logged.
|
|
162
|
+
# Its error keeps its first HELD_TEXT_CAP UTF-16 units and its output its
|
|
163
|
+
# last, each cut so as not to split a surrogate pair, with a line saying
|
|
164
|
+
# how many units were left out: "<head>\n[N more characters trimmed]" and
|
|
165
|
+
# "[N earlier characters trimmed]\n<tail>". A text of at most
|
|
166
|
+
# HELD_TEXT_CAP + HELD_TEXT_SLACK units, or that is not a String, is kept
|
|
167
|
+
# as it is, and so is every other field. The first send of an alert uses
|
|
168
|
+
# the alert as composed; only a retry reads the held copy.
|
|
169
|
+
def held_run(run)
|
|
170
|
+
error = long?(run.error, HELD_TEXT_CAP) ? keep_head(run.error, HELD_TEXT_CAP) : run.error
|
|
171
|
+
output = long?(run.output, HELD_TEXT_CAP) ? keep_tail(run.output) : run.output
|
|
172
|
+
return run if error.equal?(run.error) && output.equal?(run.output)
|
|
173
|
+
|
|
174
|
+
run.dup.tap do |held|
|
|
175
|
+
held.error = error
|
|
176
|
+
held.output = output
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# An alert as it is held in state: its run trimmed (held_run), and its
|
|
181
|
+
# message, when longer than HELD_MESSAGE_CAP + HELD_TEXT_SLACK units, cut
|
|
182
|
+
# to its first HELD_MESSAGE_CAP like a run's error. Everything else, the
|
|
183
|
+
# title and `owed` included, as it is. The first send uses the alert as
|
|
184
|
+
# composed.
|
|
185
|
+
def held_alert(alert)
|
|
186
|
+
run = alert.run.is_a?(Run) ? held_run(alert.run) : alert.run
|
|
187
|
+
message = long?(alert.message, HELD_MESSAGE_CAP) ? keep_head(alert.message, HELD_MESSAGE_CAP) : alert.message
|
|
188
|
+
return alert if run.equal?(alert.run) && message.equal?(alert.message)
|
|
189
|
+
|
|
190
|
+
alert.dup.tap do |held|
|
|
191
|
+
held.run = run
|
|
192
|
+
held.message = message
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def long?(text, cap)
|
|
197
|
+
text.is_a?(String) && JS.length16(text) > cap + HELD_TEXT_SLACK
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The first `cap` units, one fewer when the cut would split a pair.
|
|
201
|
+
def keep_head(text, cap)
|
|
202
|
+
head = JS.head16(text, cap)
|
|
203
|
+
"#{head}\n[#{JS.length16(text) - JS.length16(head)} more characters trimmed]"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The last HELD_TEXT_CAP units, one fewer when the cut would split a pair.
|
|
207
|
+
def keep_tail(text)
|
|
208
|
+
tail = JS.tail16(text, HELD_TEXT_CAP)
|
|
209
|
+
"[#{JS.length16(text) - JS.length16(tail)} earlier characters trimmed]\n#{tail}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Each configured channel's key, in the order configured: its name, with
|
|
213
|
+
# "#2", "#3", and so on for the second and later channels of the same
|
|
214
|
+
# name, so two webhooks are "webhook" and "webhook#2". A key already
|
|
215
|
+
# given to an earlier channel (a channel named "webhook#2" itself, say)
|
|
216
|
+
# is skipped for the next number up, so every key is different. A held
|
|
217
|
+
# alert names the channels that still owe it by these keys (Alert#owed).
|
|
218
|
+
def channel_keys(names)
|
|
219
|
+
seen = Hash.new(0)
|
|
220
|
+
taken = Set.new
|
|
221
|
+
names.map do |name|
|
|
222
|
+
n = seen[name] + 1
|
|
223
|
+
seen[name] = n
|
|
224
|
+
key = n == 1 ? name : "#{name}##{n}"
|
|
225
|
+
key = "#{name}##{n += 1}" while taken.include?(key)
|
|
226
|
+
taken << key
|
|
227
|
+
key
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The channels a held alert is still owed by, of those configured now
|
|
232
|
+
# (`configured`, channel_keys order): those in its `owed` that are still
|
|
233
|
+
# configured, or every configured one when it has no `owed` list (an
|
|
234
|
+
# alert held by a release before per-channel delivery, or one never
|
|
235
|
+
# sent). An entry of `owed` that is not a String is ignored. None left
|
|
236
|
+
# means the alert is done.
|
|
237
|
+
def owed_channels(alert, configured)
|
|
238
|
+
owed = alert.owed
|
|
239
|
+
return configured.dup unless owed.is_a?(Array)
|
|
240
|
+
|
|
241
|
+
configured.select { |key| owed.include?(key) }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# An alert after a send to the channels in `tried`, of which those in
|
|
245
|
+
# `failed` refused it: nil when none is left owing it, else the alert
|
|
246
|
+
# with `owed` set to those that refused, in configured order.
|
|
247
|
+
def after_send(alert, tried, failed)
|
|
248
|
+
owed = tried.select { |key| failed.include?(key) }
|
|
249
|
+
owed.empty? ? nil : alert.with_owed(owed)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# An alert without its `owed` list, as a channel is given it.
|
|
253
|
+
def without_owed(alert)
|
|
254
|
+
alert.owed? ? alert.with_owed(nil) : alert
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# `alerts` added to the undelivered queue, each with its run trimmed
|
|
258
|
+
# (held_alert) and its `owed` list as given: one with the same key as a
|
|
150
259
|
# queued alert replaces it where it stands, the rest go at the end, and
|
|
151
260
|
# only the newest MAX_UNDELIVERED stay. `dropped` counts those let go.
|
|
152
261
|
def queue_undelivered(state, alerts)
|
|
153
262
|
following = clone_state(state)
|
|
154
|
-
by_key = alerts.to_h { |alert| [alert_key(alert), alert] }
|
|
263
|
+
by_key = alerts.to_h { |alert| [alert_key(alert), held_alert(alert)] }
|
|
155
264
|
queue = following.undelivered.map { |alert| by_key.fetch(alert_key(alert), alert) }
|
|
156
265
|
known = queue.to_set { |alert| alert_key(alert) }
|
|
157
|
-
queue.concat(
|
|
266
|
+
queue.concat(by_key.values.reject { |alert| known.include?(alert_key(alert)) })
|
|
158
267
|
following.undelivered = queue.last(MAX_UNDELIVERED)
|
|
159
268
|
Held.new(state: following, dropped: [0, queue.length - MAX_UNDELIVERED].max)
|
|
160
269
|
end
|
|
@@ -163,14 +272,16 @@ module Cronwatch
|
|
|
163
272
|
# their condition, before any is sent, so a process that stops part way
|
|
164
273
|
# does not lose them: into `sending`, each with its lease ending at
|
|
165
274
|
# `until_at`, when this process sends them, or (deliver: :check) straight
|
|
166
|
-
# into the undelivered queue for a check elsewhere.
|
|
167
|
-
#
|
|
275
|
+
# into the undelivered queue for a check elsewhere. Either way the copy
|
|
276
|
+
# held has its run trimmed (held_alert) and no `owed` list, so every
|
|
277
|
+
# configured channel owes it; the first send uses the alerts as composed.
|
|
278
|
+
# `dropped` counts alerts let go past MAX_UNDELIVERED.
|
|
168
279
|
def hold_alerts(state, alerts, until_at, deferred)
|
|
169
280
|
return Held.new(state: state, dropped: 0) if alerts.empty?
|
|
170
281
|
return queue_undelivered(state, alerts) if deferred
|
|
171
282
|
|
|
172
283
|
following = clone_state(state)
|
|
173
|
-
entries = (following.sending || []) + alerts.map { |alert| { "until" => until_at, "alert" => alert } }
|
|
284
|
+
entries = (following.sending || []) + alerts.map { |alert| { "until" => until_at, "alert" => held_alert(alert) } }
|
|
174
285
|
following.sending = entries.last(MAX_UNDELIVERED)
|
|
175
286
|
Held.new(state: following, dropped: [0, entries.length - MAX_UNDELIVERED].max)
|
|
176
287
|
end
|
|
@@ -178,9 +289,9 @@ module Cronwatch
|
|
|
178
289
|
# Alerts in `sending` whose lease ran out by `now`: the process sending
|
|
179
290
|
# them stopped before it recorded how the send went. They go to the
|
|
180
291
|
# undelivered queue, where the retry sends them (with triage, which is
|
|
181
|
-
# never stored with them here) or drops them as stale
|
|
182
|
-
# not a Hash with an alert is
|
|
183
|
-
# counts as run out.
|
|
292
|
+
# never stored with them here) or drops them as stale, owed by the
|
|
293
|
+
# channels its `owed` names. An entry that is not a Hash with an alert is
|
|
294
|
+
# dropped; one without a numeric `until` counts as run out.
|
|
184
295
|
def release_sending(state, now)
|
|
185
296
|
sending = state.sending.is_a?(Array) ? state.sending : []
|
|
186
297
|
lapsed, held = sending.partition { |entry| !(entry.is_a?(Hash) && entry["until"].is_a?(Numeric) && entry["until"] > now) }
|
|
@@ -191,12 +302,15 @@ module Cronwatch
|
|
|
191
302
|
queue_undelivered(kept, lapsed.filter_map { |entry| sending_alert(entry) })
|
|
192
303
|
end
|
|
193
304
|
|
|
194
|
-
# How a send went. Delivered
|
|
195
|
-
# ones
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
|
|
305
|
+
# How a send went. Delivered alerts (no channel owes them any more) and
|
|
306
|
+
# stale ones leave the queue; failed ones (some channel still owes them,
|
|
307
|
+
# named by their `owed`) replace their queued copy, so a triage made on
|
|
308
|
+
# this attempt and the shrunken `owed` are kept, or join the queue. Every
|
|
309
|
+
# one of them leaves `sending`. last_alert_at moves to `now` when
|
|
310
|
+
# `reached` (some channel accepted an alert on this attempt; by default,
|
|
311
|
+
# when any was delivered). `dropped` counts alerts let go past
|
|
312
|
+
# MAX_UNDELIVERED.
|
|
313
|
+
def record_sent(state, delivered, failed, stale, now, reached = delivered.any?)
|
|
200
314
|
following = clone_state(state)
|
|
201
315
|
done = (delivered + stale).to_set { |alert| alert_key(alert) }
|
|
202
316
|
following.undelivered = following.undelivered.reject { |alert| done.include?(alert_key(alert)) }
|
|
@@ -206,7 +320,7 @@ module Cronwatch
|
|
|
206
320
|
alert && sent.include?(alert_key(alert))
|
|
207
321
|
end
|
|
208
322
|
following.sending = held.empty? ? nil : held
|
|
209
|
-
following.last_alert_at = now if
|
|
323
|
+
following.last_alert_at = now if reached
|
|
210
324
|
queue_undelivered(following, failed)
|
|
211
325
|
end
|
|
212
326
|
|
|
@@ -422,25 +536,55 @@ module Cronwatch
|
|
|
422
536
|
negative ? "-#{text}" : text
|
|
423
537
|
end
|
|
424
538
|
|
|
425
|
-
#
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
539
|
+
# When a run finished, or nil while it runs (a run marked timeout has finished).
|
|
540
|
+
def finished_at_of(run)
|
|
541
|
+
run.status != :running && run.finished_at.is_a?(Numeric) ? run.finished_at : nil
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Whether `run` brings the schedule up to date at `now`, so missed may
|
|
545
|
+
# close for it: judged with it as the job's last run, as on_check judges,
|
|
546
|
+
# the run the schedule wants next is not yet past its deadline. A run that
|
|
547
|
+
# started before the fire missed opened for does not cover that fire,
|
|
548
|
+
# however late it finishes, and neither does a run a source reports only
|
|
549
|
+
# once it is over whose start was before it. Every run starting now
|
|
550
|
+
# covers it. A job with no schedule, or one that no longer parses, has
|
|
551
|
+
# nothing due, so any run does.
|
|
552
|
+
def catches_up?(definition, run, now)
|
|
553
|
+
schedule = definition.schedule
|
|
554
|
+
return true if schedule.nil? || schedule == ""
|
|
555
|
+
|
|
556
|
+
begin
|
|
557
|
+
exp = Schedule.expectation(Schedule.parse(schedule, definition.timezone), run.started_at, run.started_at,
|
|
558
|
+
grace_ms(definition), finished_at_of(run))
|
|
559
|
+
exp.nil? || now <= exp.deadline
|
|
560
|
+
rescue StandardError
|
|
561
|
+
true
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Called when a run starts, or when a run a source reports is recorded.
|
|
566
|
+
# Missed and stuck are about the absence of a run, so a run starting
|
|
567
|
+
# closes them without an alert; the recovered message waits for a
|
|
568
|
+
# successful finish. Missed closes only when the run catches the schedule
|
|
569
|
+
# up (catches_up?), which a run starting now always does.
|
|
570
|
+
def on_run_start(definition, run, state, now)
|
|
429
571
|
next_state = clone_state(state)
|
|
430
|
-
close_condition(next_state, :missed)
|
|
572
|
+
close_condition(next_state, :missed) if catches_up?(definition, run, now)
|
|
431
573
|
close_condition(next_state, :stuck)
|
|
432
574
|
next_state
|
|
433
575
|
end
|
|
434
576
|
|
|
435
577
|
# Called when a run finishes with status ok, failed, or timeout. `history` is
|
|
436
|
-
# the job's earlier runs, newest first, not including this one.
|
|
578
|
+
# the job's earlier runs, newest first, not including this one. Missed
|
|
579
|
+
# closes only when the run catches the schedule up (catches_up?): a run
|
|
580
|
+
# that started before the fire missed opened for leaves it open.
|
|
437
581
|
def on_run_finish(definition, run, state, history, now)
|
|
438
582
|
next_state = clone_state(state)
|
|
439
583
|
alerts = []
|
|
440
584
|
|
|
441
585
|
if run.status == :ok
|
|
442
586
|
next_state.consecutive_failures = 0
|
|
443
|
-
close_condition(next_state, :missed)
|
|
587
|
+
close_condition(next_state, :missed) if catches_up?(definition, run, now)
|
|
444
588
|
close_condition(next_state, :stuck)
|
|
445
589
|
close_condition(next_state, :failed)
|
|
446
590
|
|
|
@@ -481,7 +625,7 @@ module Cronwatch
|
|
|
481
625
|
# failed or timeout
|
|
482
626
|
# Held at the top, as the SDK holds it: a count at the limit stays there.
|
|
483
627
|
next_state.consecutive_failures = [next_state.consecutive_failures + 1, MAX_DURATION_MS].min
|
|
484
|
-
close_condition(next_state, :missed)
|
|
628
|
+
close_condition(next_state, :missed) if catches_up?(definition, run, now)
|
|
485
629
|
threshold = [1, definition.failures_before_alert || 1].max
|
|
486
630
|
condition = run.status == :timeout ? :stuck : :failed
|
|
487
631
|
if next_state.consecutive_failures >= threshold && open_condition(next_state, condition, now)
|
|
@@ -518,15 +662,14 @@ module Cronwatch
|
|
|
518
662
|
parsed = Schedule.parse(schedule, definition.timezone)
|
|
519
663
|
grace = grace_ms(definition)
|
|
520
664
|
last_run_at = last_run&.started_at
|
|
521
|
-
exp = Schedule.expectation(parsed, last_run_at, stored.created_at, grace)
|
|
522
|
-
next_expected_at =
|
|
523
|
-
if parsed.interval? then Schedule.next_fire(parsed, stored.created_at, last_run_at)
|
|
524
|
-
else Schedule.next_fire(parsed, now, nil)
|
|
525
|
-
end
|
|
665
|
+
exp = Schedule.expectation(parsed, last_run_at, stored.created_at, grace, last_run && finished_at_of(last_run))
|
|
666
|
+
next_expected_at = parsed.interval? ? exp.due_at : Schedule.next_fire(parsed, now, nil)
|
|
526
667
|
return CheckEvaluation.new(state: next_state, alerts: alerts, next_expected_at: next_expected_at, due_at: nil) unless exp
|
|
527
668
|
|
|
528
|
-
# An interval's next run is due a period after the last one started
|
|
529
|
-
#
|
|
669
|
+
# An interval's next run is due a period after the last one started, or
|
|
670
|
+
# after it finished when it outlasted the period (Schedule.interval_due).
|
|
671
|
+
# If that run is still going, the job is busy, not late; stuck covers one
|
|
672
|
+
# that never ends.
|
|
530
673
|
if parsed.interval? && last_run&.status == :running
|
|
531
674
|
return CheckEvaluation.new(state: next_state, alerts: alerts, next_expected_at: next_expected_at, due_at: exp.due_at)
|
|
532
675
|
end
|
data/lib/cronwatch/http.rb
CHANGED
|
@@ -11,11 +11,12 @@ module Cronwatch
|
|
|
11
11
|
# @api private
|
|
12
12
|
module HTTP
|
|
13
13
|
TIMEOUT = 10
|
|
14
|
-
# The most of an answer's body read (
|
|
15
|
-
# characters of a refusal
|
|
16
|
-
#
|
|
17
|
-
# Reading stops there. A 2xx
|
|
18
|
-
|
|
14
|
+
# The most of an answer's body read (64 KiB, the SDK's ERROR_READ_MAX),
|
|
15
|
+
# far more than any secret plus the 200 characters of a refusal a channel
|
|
16
|
+
# keeps: a hostile or broken endpoint could otherwise stream gigabytes
|
|
17
|
+
# into the worker within the deadline. Reading stops there. A 2xx
|
|
18
|
+
# answer's body is not read at all.
|
|
19
|
+
MAX_BODY = 64 * 1024
|
|
19
20
|
|
|
20
21
|
Response = Struct.new(:status, :body, keyword_init: true) do
|
|
21
22
|
def ok? = status >= 200 && status < 300
|
|
@@ -38,8 +39,8 @@ module Cronwatch
|
|
|
38
39
|
# Header values are sent with the whitespace around them trimmed, as
|
|
39
40
|
# fetch trims them, so a credential read with a trailing newline still
|
|
40
41
|
# sends. Past the deadline before an answer, raises HTTP::TimeoutError;
|
|
41
|
-
# past it while the body is still arriving, returns the answer with
|
|
42
|
-
#
|
|
42
|
+
# past it while the body is still arriving, returns the answer with
|
|
43
|
+
# what of the body had come, as the SDK's errorText keeps it.
|
|
43
44
|
# A 2xx answer comes back with an empty body, unread, since no channel
|
|
44
45
|
# needs it; any other with at most MAX_BODY bytes of it.
|
|
45
46
|
def post(url, body, headers)
|
|
@@ -83,7 +84,7 @@ module Cronwatch
|
|
|
83
84
|
private
|
|
84
85
|
|
|
85
86
|
# The body, read in chunks until it ends or MAX_BODY bytes have come,
|
|
86
|
-
# whichever is first;
|
|
87
|
+
# whichever is first; what had come when the deadline passes first.
|
|
87
88
|
def read_body(conn, response, deadline)
|
|
88
89
|
body = "".b
|
|
89
90
|
conn.read_timeout = HTTP.remaining(deadline)
|
|
@@ -97,7 +98,7 @@ module Cronwatch
|
|
|
97
98
|
end
|
|
98
99
|
body
|
|
99
100
|
rescue Net::ReadTimeout, TimeoutError
|
|
100
|
-
|
|
101
|
+
body
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
|
data/lib/cronwatch/job.rb
CHANGED
|
@@ -100,7 +100,7 @@ module Cronwatch
|
|
|
100
100
|
class RunRecorder
|
|
101
101
|
# Lines are dropped from the front once the output is well past the cap;
|
|
102
102
|
# Output.redact_and_cap trims it exactly at the end.
|
|
103
|
-
KEEP =
|
|
103
|
+
KEEP = Output::CAP + Output::REDACT_EDGE + 2
|
|
104
104
|
|
|
105
105
|
attr_reader :context, :signal
|
|
106
106
|
|
|
@@ -111,7 +111,9 @@ 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
|
-
@
|
|
114
|
+
@whole = ""
|
|
115
|
+
@tail = ""
|
|
116
|
+
@logged = false
|
|
115
117
|
@metrics = {}
|
|
116
118
|
@lock = Mutex.new
|
|
117
119
|
@signal = AbortSignal.new(timeout_ms)
|
|
@@ -121,15 +123,26 @@ module Cronwatch
|
|
|
121
123
|
def log(line)
|
|
122
124
|
length = JS.length16(line)
|
|
123
125
|
@lock.synchronize do
|
|
126
|
+
separator = @logged ? "\n" : ""
|
|
127
|
+
@logged = true
|
|
128
|
+
if @whole
|
|
129
|
+
@whole = JS.length16(@whole) + separator.length + length <= 2 * Output::CAP ? @whole + separator + line : nil
|
|
130
|
+
end
|
|
131
|
+
@tail = JS.tail16(@tail + separator + JS.tail16(line, Output::CAP), Output::CAP).dup
|
|
124
132
|
if @head_size < Output::CAP
|
|
125
|
-
@head << line
|
|
133
|
+
@head << JS.head16(line, Output::CAP - @head_size).dup
|
|
126
134
|
@head_size += length + 1
|
|
127
135
|
end
|
|
136
|
+
line = Output.strip_nul(line)
|
|
137
|
+
length = JS.length16(line)
|
|
138
|
+
if length >= KEEP
|
|
139
|
+
line = JS.tail16(line, KEEP - 1).dup
|
|
140
|
+
length = JS.length16(line)
|
|
141
|
+
end
|
|
128
142
|
@lines << line
|
|
129
143
|
@size += length + 1
|
|
130
144
|
while @size > KEEP && @lines.length > 1
|
|
131
145
|
@size -= JS.length16(@lines.shift) + 1
|
|
132
|
-
@dropped = true
|
|
133
146
|
end
|
|
134
147
|
end
|
|
135
148
|
end
|
|
@@ -138,7 +151,7 @@ module Cronwatch
|
|
|
138
151
|
@lock.synchronize { @metrics[name] = value }
|
|
139
152
|
end
|
|
140
153
|
|
|
141
|
-
# The lines still held (past
|
|
154
|
+
# The lines still held (past the bounded redaction window the oldest are let go), joined and not
|
|
142
155
|
# yet capped: the client redacts them first, then caps them
|
|
143
156
|
# (Output.redact_and_cap).
|
|
144
157
|
def output
|
|
@@ -152,10 +165,9 @@ module Cronwatch
|
|
|
152
165
|
@lock.synchronize do
|
|
153
166
|
next nil if @lines.empty?
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
next all if !@dropped && JS.length16(all) <= 2 * Output::CAP
|
|
168
|
+
next @whole if @whole
|
|
157
169
|
|
|
158
|
-
"#{JS.head16(@head.join("\n"), Output::CAP)}\n#{
|
|
170
|
+
"#{JS.head16(@head.join("\n"), Output::CAP)}\n#{@tail}"
|
|
159
171
|
end
|
|
160
172
|
end
|
|
161
173
|
|