kennel 1.139.0 → 1.141.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: 33d7570c6b4d70c6d50f89d0111d8db96e6c0c0ba9273d00bb0975acfef12526
4
- data.tar.gz: ea89fdf62138917a204e9c619a7a1182acca1e8d898f22e16707d0a02685cd26
3
+ metadata.gz: e82dacccbb0c14e8b8bedd9545b782b7405ca057a02724e89aa03f0a624fa3e4
4
+ data.tar.gz: f70302b436c823c6cbcb765edbd56da1103b83fadf398c2c904372a8f73b36a7
5
5
  SHA512:
6
- metadata.gz: de01f19064531cb09615b63056b1eaa82118a4734fbdd9afa44eef56019923a736b45f9f4440da584cd0939231f7d7928f9a819cbfdb2f014336e82ee0f66c7e
7
- data.tar.gz: ec59ee486794d91f5af93e8b3f77134ff206b6f653cc424c3e337b6893fefef1a6fb92a401029e2f5fd4ca83db49c1d284bf556105ad6d38667b3825667bb973
6
+ metadata.gz: c34733bc887129a6bda1ebc96898ca46cec0346bb008af8020d4159d23db515de54d8db8813749ba7883c0417b0cecb243a530789a4a99d6aa146679996007aa
7
+ data.tar.gz: a7821e5e67899aa71a068bfabbd895f7c69a369a187b7245510fc969a69d4354bad352cfb953f64293140c6282fa21d38f0e8695ab60d9186a19b30faeddd019
@@ -30,12 +30,11 @@ module Kennel
30
30
  }.freeze
31
31
  DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
32
32
  ALLOWED_PRIORITY_CLASSES = [NilClass, Integer].freeze
33
- ALLOWED_UNLINKED = [] # rubocop:disable Style/MutableConstant placeholder for custom overrides
34
33
 
35
34
  settings(
36
35
  :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
37
36
  :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
38
- :threshold_windows, :scheduling_options, :new_host_delay, :new_group_delay, :priority, :validate_using_links, :variables, :on_missing_data,
37
+ :threshold_windows, :scheduling_options, :new_host_delay, :new_group_delay, :priority, :variables, :on_missing_data,
39
38
  :notification_preset_name
40
39
  )
41
40
 
@@ -265,6 +264,7 @@ module Kennel
265
264
  end
266
265
 
267
266
  validate_using_links(data)
267
+ validate_thresholds(data)
268
268
 
269
269
  if type == "service check" && !data[:query].to_s.include?(".by(")
270
270
  invalid! :query_must_include_by, "query must include a .by() at least .by(\"*\")"
@@ -320,16 +320,45 @@ module Kennel
320
320
  case data[:type]
321
321
  when "composite" # TODO: add slo to mirror resolve_linked_tracking_ids! logic
322
322
  ids = data[:query].tr("-", "_").scan(/\b\d+\b/)
323
- ids.reject! { |id| ALLOWED_UNLINKED.include?([tracking_id, id]) }
324
323
  if ids.any?
325
324
  invalid! :links_must_be_via_tracking_id, <<~MSG.rstrip
326
- Used #{ids} in the query, but should only use links in the form of %{<project id>:<monitor id>}
327
- If that is not possible, add `validate_using_links: ->(*){} # linked monitors are not in kennel
325
+ Use kennel ids in the query for linking monitors instead of #{ids}, for example `%{#{project.kennel_id}:<monitor id>}`
326
+ If the target monitors are not managed via kennel, add `ignored_errors: [:links_must_be_via_tracking_id] # linked monitors are not in kennel`
327
+ MSG
328
+ end
329
+ when "slo alert"
330
+ if (id = data[:query][/error_budget\("([a-f\d]+)"\)/, 1])
331
+ invalid! :links_must_be_via_tracking_id, <<~MSG
332
+ Use kennel ids in the query for linking alerts to slos instead of "#{id}", for example `error_budget("%{#{project.kennel_id}:slo_id_goes_here}")
333
+ If the target slo is not managed by kennel, then add `ignored_errors: [:links_must_be_via_tracking_id] # linked slo is not in kennel`
328
334
  MSG
329
335
  end
330
336
  else # do nothing
331
337
  end
332
338
  end
339
+
340
+ # Prevent "Warning threshold (50.0) must be less than the alert threshold (20.0) with > comparison."
341
+ def validate_thresholds(data)
342
+ return unless (warning = data.dig(:options, :thresholds, :warning))
343
+ critical = data.dig(:options, :thresholds, :critical)
344
+
345
+ case data[:query]
346
+ when /<=?\s*\S+\s*$/
347
+ if warning <= critical
348
+ invalid!(
349
+ :alert_less_than_warning,
350
+ "Warning threshold (#{warning}) must be greater than the alert threshold (#{critical}) with < comparison"
351
+ )
352
+ end
353
+ when />=?\s*\S+\s*$/
354
+ if warning >= critical
355
+ invalid!(
356
+ :alert_less_than_warning,
357
+ "Warning threshold (#{warning}) must be less than the alert threshold (#{critical}) with > comparison"
358
+ )
359
+ end
360
+ end
361
+ end
333
362
  end
334
363
  end
335
364
  end
@@ -2,14 +2,6 @@
2
2
  module Kennel
3
3
  module Models
4
4
  class Record < Base
5
- class PrepareError < StandardError
6
- def initialize(tracking_id)
7
- super("Error while preparing #{tracking_id}")
8
- end
9
- end
10
-
11
- UnvalidatedRecordError = Class.new(StandardError)
12
-
13
5
  include OptionalValidations
14
6
 
15
7
  # Apart from if you just don't like the default for some reason,
@@ -85,12 +77,12 @@ module Kennel
85
77
  end
86
78
  end
87
79
 
88
- attr_reader :project, :unfiltered_validation_errors, :filtered_validation_errors
80
+ attr_reader :project, :as_json
89
81
 
90
- def initialize(project, *args)
82
+ def initialize(project, ...)
91
83
  raise ArgumentError, "First argument must be a project, not #{project.class}" unless project.is_a?(Project)
92
84
  @project = project
93
- super(*args)
85
+ super(...)
94
86
  end
95
87
 
96
88
  def diff(actual)
@@ -141,29 +133,9 @@ module Kennel
141
133
  end
142
134
 
143
135
  def build
144
- @unfiltered_validation_errors = []
145
- json = nil
146
-
147
- begin
148
- json = build_json
149
- (id = json.delete(:id)) && json[:id] = id
150
- validate_json(json)
151
- rescue StandardError
152
- if unfiltered_validation_errors.empty?
153
- @unfiltered_validation_errors = nil
154
- raise PrepareError, safe_tracking_id # FIXME: this makes errors hard to debug when running tests
155
- end
156
- end
157
-
158
- @filtered_validation_errors = filter_validation_errors
159
- @as_json = json # Only valid if filtered_validation_errors.empty?
160
- end
161
-
162
- def as_json
163
- # A courtesy to those tests that still expect as_json to perform validation and raise on error
164
- build if @unfiltered_validation_errors.nil?
165
- raise UnvalidatedRecordError, "#{safe_tracking_id} as_json called on invalid part" if filtered_validation_errors.any?
166
-
136
+ @as_json = build_json
137
+ (id = @as_json.delete(:id)) && @as_json[:id] = id
138
+ validate_json(@as_json)
167
139
  @as_json
168
140
  end
169
141
 
@@ -184,6 +156,17 @@ module Kennel
184
156
 
185
157
  private
186
158
 
159
+ def validate_json(data)
160
+ bad = Kennel::Utils.all_keys(data).grep_v(Symbol).sort.uniq
161
+ return if bad.empty?
162
+ invalid!(
163
+ :hash_keys_must_be_symbols,
164
+ "Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
165
+ "Change these keys to be symbols (usually 'foo' => 1 --> 'foo': 1)\n" \
166
+ "#{bad.map(&:inspect).join("\n")}"
167
+ )
168
+ end
169
+
187
170
  def resolve(value, type, id_map, force:)
188
171
  return value unless tracking_id?(value)
189
172
  resolve_link(value, type, id_map, force: force)
@@ -214,10 +197,6 @@ module Kennel
214
197
  end
215
198
  end
216
199
 
217
- def invalid!(tag, message)
218
- unfiltered_validation_errors << ValidationMessage.new(tag || OptionalValidations::UNIGNORABLE, message)
219
- end
220
-
221
200
  def raise_with_location(error, message)
222
201
  super error, "#{message} for project #{project.kennel_id}"
223
202
  end
@@ -4,80 +4,89 @@ module Kennel
4
4
  ValidationMessage = Struct.new(:tag, :text)
5
5
 
6
6
  UNIGNORABLE = :unignorable
7
+ UNUSED_IGNORES = :unused_ignores
7
8
 
8
9
  def self.included(base)
9
10
  base.settings :ignored_errors
10
11
  base.defaults(ignored_errors: -> { [] })
12
+ base.attr_reader :validation_errors
11
13
  end
12
14
 
13
- def self.valid?(parts)
14
- parts_with_errors = parts.reject do |part|
15
- part.filtered_validation_errors.empty?
16
- end
15
+ def initialize(...)
16
+ super
17
+ @validation_errors = []
18
+ end
17
19
 
18
- return true if parts_with_errors.empty?
20
+ def invalid!(tag, message)
21
+ validation_errors << ValidationMessage.new(tag || OptionalValidations::UNIGNORABLE, message)
22
+ end
19
23
 
20
- example_tag = nil
24
+ def self.valid?(parts)
25
+ parts_with_errors = parts.map { |p| [p, filter_validation_errors(p)] }
26
+ return true if parts_with_errors.all? { |_, errors| errors.empty? }
21
27
 
28
+ # print errors in order
29
+ example_tag = nil
22
30
  Kennel.err.puts
23
- parts_with_errors.sort_by(&:safe_tracking_id).each do |part|
24
- part.filtered_validation_errors.each do |err|
31
+ parts_with_errors.sort_by! { |p, _| p.safe_tracking_id }
32
+ parts_with_errors.each do |part, errors|
33
+ errors.each do |err|
25
34
  Kennel.err.puts "#{part.safe_tracking_id} [#{err.tag.inspect}] #{err.text.gsub("\n", " ")}"
26
35
  example_tag = err.tag unless err.tag == :unignorable
27
36
  end
28
37
  end
29
38
  Kennel.err.puts
30
39
 
31
- Kennel.err.puts <<~MESSAGE if example_tag
32
- If a particular error cannot be fixed, it can be marked as ignored via `ignored_errors`, e.g.:
33
- Kennel::Models::Monitor.new(
34
- ...,
35
- ignored_errors: [#{example_tag.inspect}]
36
- )
40
+ if example_tag
41
+ Kennel.err.puts <<~MESSAGE
42
+ If a particular error cannot be fixed, it can be marked as ignored via `ignored_errors`, e.g.:
43
+ Kennel::Models::Monitor.new(
44
+ ...,
45
+ ignored_errors: [#{example_tag.inspect}]
46
+ )
37
47
 
38
- MESSAGE
48
+ MESSAGE
49
+ end
39
50
 
40
51
  false
41
52
  end
42
53
 
43
- private
44
-
45
- def validate_json(data)
46
- bad = Kennel::Utils.all_keys(data).grep_v(Symbol).sort.uniq
47
- return if bad.empty?
48
- invalid!(
49
- :hash_keys_must_be_symbols,
50
- "Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
51
- "Change these keys to be symbols (usually 'foo' => 1 --> 'foo': 1)\n" \
52
- "#{bad.map(&:inspect).join("\n")}"
53
- )
54
- end
54
+ def self.filter_validation_errors(part)
55
+ errors = part.validation_errors
56
+ ignored_tags = part.ignored_errors
55
57
 
56
- def filter_validation_errors
57
- if unfiltered_validation_errors.empty?
58
- if ignored_errors.empty?
58
+ if errors.empty? # 95% case, so keep it fast
59
+ if ignored_tags.empty? || ignored_tags.include?(UNUSED_IGNORES)
59
60
  []
60
61
  else
61
- [ValidationMessage.new(UNIGNORABLE, "`ignored_errors` is non-empty, but there are no errors to ignore. Remove `ignored_errors`")]
62
+ # tell users to remove the whole line and not just an element
63
+ [
64
+ ValidationMessage.new(
65
+ UNUSED_IGNORES,
66
+ "`ignored_errors` is non-empty, but there are no errors to ignore. Remove `ignored_errors`"
67
+ )
68
+ ]
62
69
  end
63
70
  else
64
- to_report =
65
- if ENV["NO_IGNORED_ERRORS"]
66
- # Turn off all suppressions, to see what errors are actually being suppressed
67
- unfiltered_validation_errors
71
+ reported_errors =
72
+ if ENV["NO_IGNORED_ERRORS"] # let users see what errors are suppressed
73
+ errors
68
74
  else
69
- unfiltered_validation_errors.reject do |err|
70
- err.tag != UNIGNORABLE && ignored_errors.include?(err.tag)
71
- end
75
+ errors.select { |err| err.tag == UNIGNORABLE || !ignored_tags.include?(err.tag) }
72
76
  end
73
77
 
74
- unused_ignores = ignored_errors - unfiltered_validation_errors.map(&:tag)
75
-
76
- if unused_ignores.any?
77
- to_report << ValidationMessage.new(UNIGNORABLE, "Unused ignores #{unused_ignores.map(&:inspect).sort.uniq.join(" ")}. Remove these from `ignored_errors`")
78
+ # let users know when they can remove an ignore ... unless they don't care (for example for a generic monitor)
79
+ unless ignored_tags.include?(UNUSED_IGNORES)
80
+ unused_ignored_tags = ignored_tags - errors.map(&:tag)
81
+ if unused_ignored_tags.any?
82
+ reported_errors << ValidationMessage.new(
83
+ UNUSED_IGNORES,
84
+ "Unused ignores #{unused_ignored_tags.map(&:inspect).sort.uniq.join(" ")}. Remove these from `ignored_errors`"
85
+ )
86
+ end
78
87
  end
79
88
 
80
- to_report
89
+ reported_errors
81
90
  end
82
91
  end
83
92
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.139.0"
3
+ VERSION = "1.141.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -127,7 +127,7 @@ module Kennel
127
127
  Utils.parallel(parts, &:build)
128
128
  end
129
129
 
130
- OptionalValidations.valid?(parts) or raise GenerationAbortedError
130
+ OptionalValidations.valid?(parts) || raise(GenerationAbortedError)
131
131
 
132
132
  parts
133
133
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kennel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.139.0
4
+ version: 1.141.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-26 00:00:00.000000000 Z
11
+ date: 2023-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs