cronwatch 0.3.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/lib/cronwatch/abort_signal.rb +45 -0
- data/lib/cronwatch/active_record.rb +11 -0
- data/lib/cronwatch/alerts/console.rb +22 -0
- data/lib/cronwatch/alerts/custom.rb +26 -0
- data/lib/cronwatch/alerts/discord.rb +52 -0
- data/lib/cronwatch/alerts/slack.rb +58 -0
- data/lib/cronwatch/alerts/webhook.rb +46 -0
- data/lib/cronwatch/client.rb +925 -0
- data/lib/cronwatch/cron_pattern.rb +277 -0
- data/lib/cronwatch/duration.rb +77 -0
- data/lib/cronwatch/environment.rb +29 -0
- data/lib/cronwatch/evaluate.rb +345 -0
- data/lib/cronwatch/flight.rb +42 -0
- data/lib/cronwatch/format.rb +89 -0
- data/lib/cronwatch/http.rb +52 -0
- data/lib/cronwatch/job.rb +144 -0
- data/lib/cronwatch/js.rb +188 -0
- data/lib/cronwatch/monitored.rb +259 -0
- data/lib/cronwatch/output.rb +199 -0
- data/lib/cronwatch/rails/active_job.rb +55 -0
- data/lib/cronwatch/rails/check_job.rb +32 -0
- data/lib/cronwatch/rails/railtie.rb +37 -0
- data/lib/cronwatch/rails/tasks.rb +12 -0
- data/lib/cronwatch/rails.rb +35 -0
- data/lib/cronwatch/schedule.rb +191 -0
- data/lib/cronwatch/scheduler.rb +763 -0
- data/lib/cronwatch/serialize.rb +51 -0
- data/lib/cronwatch/sidekiq.rb +129 -0
- data/lib/cronwatch/stats.rb +23 -0
- data/lib/cronwatch/stores/active_record.rb +397 -0
- data/lib/cronwatch/stores/memory.rb +163 -0
- data/lib/cronwatch/ticker.rb +59 -0
- data/lib/cronwatch/triage/anthropic.rb +134 -0
- data/lib/cronwatch/types.rb +341 -0
- data/lib/cronwatch/version.rb +6 -0
- data/lib/cronwatch/walker.rb +137 -0
- data/lib/cronwatch/web/app.rb +484 -0
- data/lib/cronwatch/web/html.rb +314 -0
- data/lib/cronwatch/web.rb +17 -0
- data/lib/cronwatch/zone.rb +72 -0
- data/lib/cronwatch.rb +96 -0
- data/lib/generators/cronwatch/install/install_generator.rb +176 -0
- metadata +104 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fugit"
|
|
4
|
+
require_relative "zone"
|
|
5
|
+
require_relative "walker"
|
|
6
|
+
|
|
7
|
+
module Cronwatch
|
|
8
|
+
# A cron expression's fields, and the next time they match. What is
|
|
9
|
+
# accepted, and which times match, follow croner; Fugit parses the values.
|
|
10
|
+
class CronPattern
|
|
11
|
+
NICKNAMES = {
|
|
12
|
+
"@yearly" => "0 0 1 1 *", "@annually" => "0 0 1 1 *", "@monthly" => "0 0 1 * *", "@weekly" => "0 0 * * 0",
|
|
13
|
+
"@daily" => "0 0 * * *", "@midnight" => "0 0 * * *", "@hourly" => "0 * * * *",
|
|
14
|
+
}.freeze
|
|
15
|
+
SIZES = { second: 60, minute: 60, hour: 24, day: 31, month: 12, dayOfWeek: 7 }.freeze
|
|
16
|
+
# Croner's bits for "the nth weekday of the month"; 32 is the last one, 63 any.
|
|
17
|
+
NTH = [1, 2, 4, 8, 16].freeze
|
|
18
|
+
LAST_NTH = 32
|
|
19
|
+
ANY_NTH = 63
|
|
20
|
+
|
|
21
|
+
attr_reader :seconds, :minutes, :hours, :days, :months, :weekdays, :last_day_of_month, :star_dom, :star_dow,
|
|
22
|
+
:and_logic
|
|
23
|
+
|
|
24
|
+
def self.compile(text)
|
|
25
|
+
new(text)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(text)
|
|
29
|
+
fields, dom, dow = croner_fields(text)
|
|
30
|
+
cron = begin
|
|
31
|
+
Fugit::Cron.parse(fields.each_with_index.map { |f, i| i == 3 ? dom : (i == 5 ? dow : f) }.join(" "))
|
|
32
|
+
rescue StandardError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
# Croner accepts a few forms Fugit does not read (a range with #, a day of
|
|
36
|
+
# the month with L after it, a date no month has). Those are refused.
|
|
37
|
+
raise ArgumentError, "CronPattern: '#{text}' uses a form the Ruby port does not read" unless cron
|
|
38
|
+
|
|
39
|
+
@seconds = flags(cron.seconds, 60, 0)
|
|
40
|
+
@minutes = flags(cron.minutes, 60, 0)
|
|
41
|
+
@hours = flags(cron.hours, 24, 0)
|
|
42
|
+
@days = Array.new(31, false)
|
|
43
|
+
if cron.monthdays.nil?
|
|
44
|
+
@days.fill(true)
|
|
45
|
+
else
|
|
46
|
+
cron.monthdays.each { |d| @days[d - 1] = true if d.positive? }
|
|
47
|
+
end
|
|
48
|
+
@months = flags(cron.months, 12, 1)
|
|
49
|
+
@weekdays = Array.new(7, 0)
|
|
50
|
+
if cron.weekdays.nil?
|
|
51
|
+
@weekdays.fill(ANY_NTH)
|
|
52
|
+
else
|
|
53
|
+
cron.weekdays.each do |day, nth|
|
|
54
|
+
day = 0 if day == 7
|
|
55
|
+
@weekdays[day] =
|
|
56
|
+
if nth.nil? then ANY_NTH
|
|
57
|
+
elsif nth == -1 then @weekdays[day] | LAST_NTH
|
|
58
|
+
else @weekdays[day] | NTH[nth - 1]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The first fire strictly after epoch milliseconds `from`, as epoch
|
|
66
|
+
# milliseconds, or nil when there is none before the year 3000.
|
|
67
|
+
def next_after(from, timezone)
|
|
68
|
+
wall = Zone.wall(from.div(1000), timezone)
|
|
69
|
+
date = Walker.new(self, wall)
|
|
70
|
+
date.second += 1
|
|
71
|
+
date.apply
|
|
72
|
+
found = date.search
|
|
73
|
+
found && Zone.to_utc(found, timezone) * 1000
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def flags(values, size, base)
|
|
79
|
+
return Array.new(size, true) if values.nil?
|
|
80
|
+
|
|
81
|
+
out = Array.new(size, false)
|
|
82
|
+
values.each { |v| out[v - base] = true }
|
|
83
|
+
out
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Applies croner's reading of the text: nicknames, five fields padded with
|
|
87
|
+
# seconds, month and weekday names, L, ?, the + for AND, and the checks
|
|
88
|
+
# croner makes on every field, with its messages. Returns the six fields,
|
|
89
|
+
# and the day-of-month and day-of-week fields to hand Fugit.
|
|
90
|
+
def croner_fields(text)
|
|
91
|
+
pattern = text
|
|
92
|
+
pattern = JS.trim(NICKNAMES.fetch(JS.trim(pattern).downcase) { nickname_or_text(pattern) }) if pattern.include?("@")
|
|
93
|
+
parts = pattern.scan(JS::NOT_SPACE)
|
|
94
|
+
parts = [""] if parts.empty?
|
|
95
|
+
if parts.length < 5 || parts.length > 7
|
|
96
|
+
raise ArgumentError, "CronPattern: invalid configuration format ('#{pattern}'), exactly five, six, or seven space separated parts are required."
|
|
97
|
+
end
|
|
98
|
+
raise ArgumentError, "CronPattern: a seventh (year) field is not supported by the Ruby port" if parts.length == 7
|
|
99
|
+
|
|
100
|
+
parts.unshift("0") if parts.length == 5
|
|
101
|
+
@last_day_of_month = false
|
|
102
|
+
fugit_dom = parts[3]
|
|
103
|
+
if parts[3].upcase == "LW"
|
|
104
|
+
raise ArgumentError, "CronPattern: LW is not supported by the Ruby port"
|
|
105
|
+
elsif parts[3].upcase.include?("L")
|
|
106
|
+
parts[3] = parts[3].gsub(/L/i, "")
|
|
107
|
+
@last_day_of_month = true
|
|
108
|
+
end
|
|
109
|
+
@star_dom = parts[3] == "*"
|
|
110
|
+
parts[4] = month_names(parts[4]) if parts[4].length >= 3
|
|
111
|
+
parts[5] = day_names(parts[5]) if parts[5].length >= 3
|
|
112
|
+
@and_logic = false
|
|
113
|
+
if parts[5].start_with?("+")
|
|
114
|
+
@and_logic = true
|
|
115
|
+
parts[5] = parts[5][1..]
|
|
116
|
+
raise ArgumentError, "CronPattern: Day-of-week field cannot be empty after '+' modifier." if parts[5].empty?
|
|
117
|
+
end
|
|
118
|
+
@star_dow = parts[5] == "*"
|
|
119
|
+
if pattern.include?("?")
|
|
120
|
+
parts.map! { |p| p.tr("?", "*") }
|
|
121
|
+
fugit_dom = fugit_dom.tr("?", "*")
|
|
122
|
+
end
|
|
123
|
+
parts.each_with_index do |part, i|
|
|
124
|
+
illegal = i == 3 ? %r{[^/*0-9,\-WwLl]+} : (i == 5 ? %r{[^/*0-9,\-#Ll]+} : %r{[^/*0-9,\-]+})
|
|
125
|
+
raise ArgumentError, "CronPattern: configuration entry #{i} (#{part}) contains illegal characters." if illegal.match?(part)
|
|
126
|
+
end
|
|
127
|
+
raise ArgumentError, "CronPattern: W is not supported by the Ruby port" if parts[3].match?(/w/i)
|
|
128
|
+
|
|
129
|
+
check(:second, parts[0], 0)
|
|
130
|
+
check(:minute, parts[1], 0)
|
|
131
|
+
check(:hour, parts[2], 0)
|
|
132
|
+
check(:day, parts[3], -1)
|
|
133
|
+
check(:month, parts[4], -1)
|
|
134
|
+
check(:dayOfWeek, parts[5], 0)
|
|
135
|
+
# Croner writes the last Friday as 5L, Fugit as 5#-1.
|
|
136
|
+
fugit_dow = parts[5].gsub(/(\d+)L/i) { "#{Regexp.last_match(1)}#-1" }
|
|
137
|
+
fugit_dom = fugit_dom.gsub(/L/i, "L")
|
|
138
|
+
[parts, fugit_dom, fugit_dow]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def nickname_or_text(pattern)
|
|
142
|
+
raise ArgumentError, "CronPattern: @reboot is not supported in this environment. This is an event-based trigger that requires system startup detection." if JS.trim(pattern).downcase == "@reboot"
|
|
143
|
+
|
|
144
|
+
pattern
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def month_names(text)
|
|
148
|
+
%w[jan feb mar apr may jun jul aug sep oct nov dec].each_with_index.reduce(text) do |out, (name, i)|
|
|
149
|
+
out.gsub(/#{name}/i, (i + 1).to_s)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def day_names(text)
|
|
154
|
+
text = text.gsub(/-sun/i, "-7")
|
|
155
|
+
%w[sun mon tue wed thu fri sat].each_with_index.reduce(text) { |out, (name, i)| out.gsub(/#{name}/i, i.to_s) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Croner's partToArray, keeping only its checks.
|
|
159
|
+
def check(type, text, offset)
|
|
160
|
+
if text == "" && !(type == :day && @last_day_of_month)
|
|
161
|
+
raise ArgumentError, "CronPattern: configuration entry #{type} (#{text}) is empty, check for trailing spaces."
|
|
162
|
+
end
|
|
163
|
+
return if text == "*"
|
|
164
|
+
|
|
165
|
+
items = text.split(",", -1)
|
|
166
|
+
if items.length > 1
|
|
167
|
+
items.each { |item| check(type, item, offset) }
|
|
168
|
+
elsif text.include?("-") && text.include?("/")
|
|
169
|
+
check_range_with_step(type, text, offset)
|
|
170
|
+
elsif text.include?("-")
|
|
171
|
+
check_range(type, text, offset)
|
|
172
|
+
elsif text.include?("/")
|
|
173
|
+
check_step(type, text, offset)
|
|
174
|
+
elsif text != ""
|
|
175
|
+
value, nth = extract_nth(text, type)
|
|
176
|
+
number = parse_int(value)
|
|
177
|
+
raise ArgumentError, "CronPattern: #{type} is not a number: '#{text}'" if number.nil?
|
|
178
|
+
|
|
179
|
+
check_value(type, number + offset, nth)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def check_range_with_step(type, text, offset)
|
|
184
|
+
value, nth = extract_nth(text, type)
|
|
185
|
+
match = %r{\A(\d+)-(\d+)/(\d+)\z}.match(value)
|
|
186
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal range with stepping: '#{text}'" unless match
|
|
187
|
+
|
|
188
|
+
from = match[1].to_i + offset
|
|
189
|
+
to = match[2].to_i + offset
|
|
190
|
+
step = match[3].to_i
|
|
191
|
+
check_range_bounds(from, to, step, type, text)
|
|
192
|
+
from.step(to, step) { |v| check_value(type, v, nth) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def check_range(type, text, offset)
|
|
196
|
+
value, nth = extract_nth(text, type)
|
|
197
|
+
bounds = value.split("-", -1)
|
|
198
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal range: '#{text}'" unless bounds.length == 2
|
|
199
|
+
|
|
200
|
+
from = parse_int(bounds[0])
|
|
201
|
+
to = parse_int(bounds[1])
|
|
202
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal lower range (NaN)" if from.nil?
|
|
203
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal upper range (NaN)" if to.nil?
|
|
204
|
+
|
|
205
|
+
check_range_bounds(from + offset, to + offset, nil, type, text)
|
|
206
|
+
(from + offset).upto(to + offset) { |v| check_value(type, v, nth) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def check_step(type, text, _offset)
|
|
210
|
+
value, nth = extract_nth(text, type)
|
|
211
|
+
parts = value.split("/", -1)
|
|
212
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal stepping: '#{text}'" unless parts.length == 2
|
|
213
|
+
if parts[0] == ""
|
|
214
|
+
raise ArgumentError, "CronPattern: Syntax error, stepping with missing prefix ('#{text}') is not allowed. Use wildcard (*/step) or range (min-max/step) instead."
|
|
215
|
+
end
|
|
216
|
+
if parts[0] != "*"
|
|
217
|
+
raise ArgumentError, "CronPattern: Syntax error, stepping with numeric prefix ('#{text}') is not allowed. Use wildcard (*/step) or range (min-max/step) instead."
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
step = parse_int(parts[1])
|
|
221
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal stepping: (NaN)" if step.nil?
|
|
222
|
+
|
|
223
|
+
check_range_bounds(0, SIZES[type] - 1, step, type, text)
|
|
224
|
+
0.step(SIZES[type] - 1, step) { |v| check_value(type, v, nth) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def check_range_bounds(from, to, step, type, text)
|
|
228
|
+
raise ArgumentError, "CronPattern: From value is larger than to value: '#{text}'" if from > to
|
|
229
|
+
return if step.nil?
|
|
230
|
+
raise ArgumentError, "CronPattern: Syntax error, illegal stepping: 0" if step.zero?
|
|
231
|
+
if step > SIZES[type]
|
|
232
|
+
raise ArgumentError, "CronPattern: Syntax error, steps cannot be greater than maximum value of part (#{SIZES[type]})"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def extract_nth(text, type)
|
|
237
|
+
if text.include?("#")
|
|
238
|
+
raise ArgumentError, "CronPattern: nth (#) only allowed in day-of-week field" unless type == :dayOfWeek
|
|
239
|
+
|
|
240
|
+
pieces = text.split("#", -1)
|
|
241
|
+
[pieces[0], pieces[1]]
|
|
242
|
+
elsif text.upcase.end_with?("L")
|
|
243
|
+
unless type == :dayOfWeek
|
|
244
|
+
raise ArgumentError, "CronPattern: L modifier only allowed in day-of-week field (use L alone for day-of-month)"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
[text[0...-1], "L"]
|
|
248
|
+
else
|
|
249
|
+
[text, nil]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def check_value(type, value, nth)
|
|
254
|
+
if type == :dayOfWeek
|
|
255
|
+
value = 0 if value == 7
|
|
256
|
+
raise ArgumentError, "CronPattern: Invalid value for dayOfWeek: #{value}" if value.negative? || value > 6
|
|
257
|
+
# An empty nth ("1#") falls back to any, as `"" || 63` does in croner.
|
|
258
|
+
return if nth.nil? || nth == "" || nth.upcase == "L"
|
|
259
|
+
|
|
260
|
+
n = nth.match?(/\A\d+\z/) ? nth.to_i : nil
|
|
261
|
+
return if n && n < 6 && n.positive?
|
|
262
|
+
|
|
263
|
+
raise ArgumentError, "CronPattern: nth weekday out of range, should be 1-5 or L. Value: #{nth}, Type: string"
|
|
264
|
+
end
|
|
265
|
+
limit = SIZES[type]
|
|
266
|
+
return if value >= 0 && value < limit
|
|
267
|
+
|
|
268
|
+
raise ArgumentError, "CronPattern: Invalid value for #{type}: #{value}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# JavaScript's parseInt, for the characters a field may hold: nil for NaN.
|
|
272
|
+
def parse_int(text)
|
|
273
|
+
match = /\A[+-]?\d+/.match(text)
|
|
274
|
+
match && match[0].to_i
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronwatch
|
|
4
|
+
# "15m", "1h30m", "90s", "2d", or a number of milliseconds.
|
|
5
|
+
module Duration
|
|
6
|
+
UNIT_MS = { "ms" => 1, "s" => 1000, "m" => 60_000, "h" => 3_600_000, "d" => 86_400_000, "w" => 604_800_000 }.freeze
|
|
7
|
+
PART = Regexp.new("(\\d+(?:\\.\\d+)?)[#{JS::WHITESPACE}]*(ms|s|m|h|d|w)")
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# An ActiveSupport::Duration (15.minutes), when ActiveSupport is loaded.
|
|
12
|
+
def active_support?(value)
|
|
13
|
+
defined?(::ActiveSupport::Duration) && value.instance_of?(::ActiveSupport::Duration)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
|
17
|
+
# strings such as "1h30m". Whitespace between parts is fine. An
|
|
18
|
+
# ActiveSupport::Duration (15.minutes) is read as what it says.
|
|
19
|
+
def parse(value, label = "duration")
|
|
20
|
+
if active_support?(value)
|
|
21
|
+
return JS.round(value.to_f * 1000)
|
|
22
|
+
end
|
|
23
|
+
if value.is_a?(Numeric)
|
|
24
|
+
raise ArgumentError, "#{label} must be a non-negative number of milliseconds" if !JS.finite?(value) || value.negative?
|
|
25
|
+
|
|
26
|
+
return value
|
|
27
|
+
end
|
|
28
|
+
unless value.is_a?(String)
|
|
29
|
+
raise ArgumentError, "#{label} \"#{value}\" is not a duration like \"15m\", \"1h30m\" or \"90s\""
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
text = JS.trim(value).downcase
|
|
33
|
+
raise ArgumentError, "#{label} is empty" if text.empty?
|
|
34
|
+
|
|
35
|
+
total = 0
|
|
36
|
+
consumed = +""
|
|
37
|
+
text.scan(PART) do
|
|
38
|
+
match = Regexp.last_match
|
|
39
|
+
total += Float(match[1]) * UNIT_MS.fetch(match[2])
|
|
40
|
+
consumed << match[0]
|
|
41
|
+
end
|
|
42
|
+
if consumed.gsub(JS::SPACES, "") != text.gsub(JS::SPACES, "")
|
|
43
|
+
raise ArgumentError, "#{label} \"#{value}\" is not a duration like \"15m\", \"1h30m\" or \"90s\""
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
JS.round(total)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 90000 -> "1m 30s". For messages, not for parsing back.
|
|
50
|
+
def format(ms)
|
|
51
|
+
return "?" unless JS.finite?(ms)
|
|
52
|
+
return "#{JS.round(ms)}ms" if ms < 1000
|
|
53
|
+
|
|
54
|
+
parts = []
|
|
55
|
+
rest = JS.round(ms / 1000.0)
|
|
56
|
+
[["d", 86_400], ["h", 3_600], ["m", 60], ["s", 1]].each do |unit, size|
|
|
57
|
+
if rest >= size
|
|
58
|
+
n = rest.div(size)
|
|
59
|
+
rest -= n * size
|
|
60
|
+
parts << "#{n}#{unit}"
|
|
61
|
+
end
|
|
62
|
+
break if parts.length == 2
|
|
63
|
+
end
|
|
64
|
+
parts.empty? ? "0s" : parts.join(" ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# "5m ago", "in 2h". Relative to `now`.
|
|
68
|
+
def relative(at, now)
|
|
69
|
+
diff = at - now
|
|
70
|
+
abs = diff.abs
|
|
71
|
+
return "now" if abs < 5_000
|
|
72
|
+
|
|
73
|
+
text = format(abs)
|
|
74
|
+
diff.negative? ? "#{text} ago" : "in #{text}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronwatch
|
|
4
|
+
# The app's environment, read one way everywhere: Rails.env when Rails is
|
|
5
|
+
# loaded, otherwise RAILS_ENV, then RACK_ENV. The SDK reads NODE_ENV.
|
|
6
|
+
module Environment
|
|
7
|
+
DEVELOPMENT = %w[development test].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# "production", "development", ... or nil when nothing names one.
|
|
12
|
+
def name
|
|
13
|
+
if defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
14
|
+
env = ::Rails.env.to_s
|
|
15
|
+
return env unless env.empty?
|
|
16
|
+
end
|
|
17
|
+
[ENV.fetch("RAILS_ENV", nil), ENV.fetch("RACK_ENV", nil)].find { |value| value && !value.empty? }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Development or test: Cronwatch::Web without a token serves only then.
|
|
21
|
+
def development?
|
|
22
|
+
DEVELOPMENT.include?(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def production?
|
|
26
|
+
name == "production"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|