kennel 2.2.1 → 2.22.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 +75 -4
- data/lib/kennel/api.rb +103 -42
- data/lib/kennel/filter.rb +7 -11
- data/lib/kennel/importer.rb +14 -1
- data/lib/kennel/models/dashboard.rb +4 -3
- data/lib/kennel/models/monitor.rb +122 -55
- data/lib/kennel/models/project.rb +20 -4
- data/lib/kennel/models/record.rb +2 -6
- data/lib/kennel/models/slo.rb +41 -7
- data/lib/kennel/models/synthetic_test.rb +3 -0
- data/lib/kennel/models/team.rb +5 -3
- data/lib/kennel/parts_serializer.rb +30 -11
- data/lib/kennel/projects_provider.rb +3 -2
- data/lib/kennel/subclass_tracking.rb +8 -0
- data/lib/kennel/syncer/matched_expected.rb +7 -1
- data/lib/kennel/syncer/plan_printer.rb +1 -1
- data/lib/kennel/syncer/resolver.rb +1 -1
- data/lib/kennel/syncer.rb +25 -27
- data/lib/kennel/tasks/dump.rb +54 -0
- data/lib/kennel/tasks/import.rb +17 -0
- data/lib/kennel/tasks/nodata.rb +53 -0
- data/lib/kennel/tasks/tracking_id.rb +14 -0
- data/lib/kennel/tasks/validate_mentions.rb +41 -0
- data/lib/kennel/tasks/validate_plan.rb +114 -0
- data/lib/kennel/tasks.rb +9 -159
- data/lib/kennel/utils.rb +3 -2
- data/lib/kennel/version.rb +1 -1
- data/lib/kennel.rb +13 -7
- data/template/Readme.md +66 -4
- metadata +22 -20
|
@@ -6,7 +6,8 @@ module Kennel
|
|
|
6
6
|
|
|
7
7
|
OPTIONAL_SERVICE_CHECK_THRESHOLDS = [:ok, :warning].freeze
|
|
8
8
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
|
|
9
|
-
:multi, :matching_downtimes, :overall_state_modified, :overall_state, :restricted_roles, :draft_status
|
|
9
|
+
:multi, :matching_downtimes, :overall_state_modified, :overall_state, :restricted_roles, :draft_status, :assets,
|
|
10
|
+
:enable_logs_sample
|
|
10
11
|
]
|
|
11
12
|
TRACKING_FIELD = :message
|
|
12
13
|
|
|
@@ -25,19 +26,27 @@ module Kennel
|
|
|
25
26
|
group_retention_duration: nil,
|
|
26
27
|
groupby_simple_monitor: false,
|
|
27
28
|
variables: nil,
|
|
28
|
-
on_missing_data:
|
|
29
|
+
on_missing_data: nil,
|
|
29
30
|
notification_preset_name: nil,
|
|
30
|
-
notify_by: nil
|
|
31
|
+
notify_by: nil,
|
|
32
|
+
include_tags: true
|
|
31
33
|
}.freeze
|
|
32
34
|
DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
|
|
33
35
|
ALLOWED_PRIORITY_CLASSES = [NilClass, Integer].freeze
|
|
34
36
|
SKIP_NOTIFY_NO_DATA_TYPES = ["event alert", "event-v2 alert", "log alert"].freeze
|
|
37
|
+
ON_MISSING_DATA_UNSUPPORTED_TYPES = ["composite", "datadog-usage alert"].freeze
|
|
38
|
+
MINUTES_PER_UNIT = {
|
|
39
|
+
"m" => 1,
|
|
40
|
+
"h" => 60,
|
|
41
|
+
"d" => 60 * 24,
|
|
42
|
+
"w" => 60 * 24 * 7
|
|
43
|
+
}.freeze
|
|
35
44
|
|
|
36
45
|
settings(
|
|
37
46
|
:query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
|
|
38
47
|
:ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
|
|
39
48
|
:threshold_windows, :scheduling_options, :new_host_delay, :new_group_delay, :group_retention_duration, :priority,
|
|
40
|
-
:variables, :on_missing_data, :notification_preset_name, :notify_by
|
|
49
|
+
:variables, :on_missing_data, :notification_preset_name, :notify_by, :include_tags
|
|
41
50
|
)
|
|
42
51
|
|
|
43
52
|
defaults(
|
|
@@ -49,13 +58,12 @@ module Kennel
|
|
|
49
58
|
# datadog UI sets this to false by default, but true is safer
|
|
50
59
|
# except for log alerts which will always have "no error" gaps and should default to false
|
|
51
60
|
notify_no_data: -> { !SKIP_NOTIFY_NO_DATA_TYPES.include?(type) },
|
|
52
|
-
no_data_timeframe: -> {
|
|
61
|
+
no_data_timeframe: -> { MONITOR_OPTION_DEFAULTS.fetch(:no_data_timeframe) },
|
|
53
62
|
notify_audit: -> { MONITOR_OPTION_DEFAULTS.fetch(:notify_audit) },
|
|
54
63
|
new_host_delay: -> { MONITOR_OPTION_DEFAULTS.fetch(:new_host_delay) },
|
|
55
64
|
new_group_delay: -> { nil },
|
|
56
65
|
group_retention_duration: -> { MONITOR_OPTION_DEFAULTS.fetch(:group_retention_duration) },
|
|
57
66
|
tags: -> { @project.tags },
|
|
58
|
-
timeout_h: -> { MONITOR_OPTION_DEFAULTS.fetch(:timeout_h) },
|
|
59
67
|
evaluation_delay: -> { MONITOR_OPTION_DEFAULTS.fetch(:evaluation_delay) },
|
|
60
68
|
critical_recovery: -> { nil },
|
|
61
69
|
warning_recovery: -> { nil },
|
|
@@ -66,10 +74,13 @@ module Kennel
|
|
|
66
74
|
on_missing_data: -> { MONITOR_OPTION_DEFAULTS.fetch(:on_missing_data) },
|
|
67
75
|
notification_preset_name: -> { MONITOR_OPTION_DEFAULTS.fetch(:notification_preset_name) },
|
|
68
76
|
notify_by: -> { MONITOR_OPTION_DEFAULTS.fetch(:notify_by) },
|
|
69
|
-
require_full_window: -> { false }
|
|
77
|
+
require_full_window: -> { false },
|
|
78
|
+
include_tags: -> { MONITOR_OPTION_DEFAULTS.fetch(:include_tags) }
|
|
70
79
|
)
|
|
71
80
|
|
|
72
81
|
def build_json
|
|
82
|
+
no_data_options = configure_no_data
|
|
83
|
+
|
|
73
84
|
data = super.merge(
|
|
74
85
|
name: "#{name}#{LOCK}",
|
|
75
86
|
type: type,
|
|
@@ -79,42 +90,22 @@ module Kennel
|
|
|
79
90
|
priority: priority,
|
|
80
91
|
options: {
|
|
81
92
|
timeout_h: timeout_h,
|
|
82
|
-
|
|
83
|
-
no_data_timeframe: notify_no_data ? no_data_timeframe : nil,
|
|
93
|
+
**no_data_options.except(:on_missing_data),
|
|
84
94
|
notify_audit: notify_audit,
|
|
85
95
|
require_full_window: require_full_window,
|
|
86
96
|
new_host_delay: new_host_delay,
|
|
87
97
|
new_group_delay: new_group_delay,
|
|
88
|
-
include_tags:
|
|
98
|
+
include_tags: include_tags,
|
|
89
99
|
escalation_message: Utils.presence(escalation_message.strip),
|
|
90
100
|
evaluation_delay: evaluation_delay,
|
|
91
|
-
locked: false, # deprecated: setting this to true will likely fail
|
|
92
101
|
renotify_interval: renotify_interval || 0,
|
|
93
|
-
variables: variables
|
|
102
|
+
variables: variables,
|
|
103
|
+
**configure_thresholds,
|
|
104
|
+
**no_data_options.slice(:on_missing_data) # moved here to avoid generated diff
|
|
94
105
|
}
|
|
95
106
|
)
|
|
96
107
|
|
|
97
108
|
options = data[:options]
|
|
98
|
-
if data.fetch(:type) != "composite"
|
|
99
|
-
thresholds = (options[:thresholds] = { critical: critical })
|
|
100
|
-
|
|
101
|
-
# warning, ok, critical_recovery, and warning_recovery are optional
|
|
102
|
-
[:warning, :ok, :critical_recovery, :warning_recovery].each do |key|
|
|
103
|
-
if (value = send(key))
|
|
104
|
-
thresholds[key] = value
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
thresholds[:critical] = critical unless
|
|
109
|
-
case data.fetch(:type)
|
|
110
|
-
when "service check"
|
|
111
|
-
# avoid diff for default values of 1
|
|
112
|
-
OPTIONAL_SERVICE_CHECK_THRESHOLDS.each { |t| thresholds[t] ||= 1 }
|
|
113
|
-
when "query alert"
|
|
114
|
-
# metric and query values are stored as float by datadog
|
|
115
|
-
thresholds.each { |k, v| thresholds[k] = Float(v) }
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
109
|
|
|
119
110
|
# set without causing lots of nulls to be stored
|
|
120
111
|
if (notify_by_value = notify_by)
|
|
@@ -145,30 +136,54 @@ module Kennel
|
|
|
145
136
|
# Add in statuses where we would re notify on. Possible values: alert, no data, warn
|
|
146
137
|
if options[:renotify_interval] != 0
|
|
147
138
|
statuses = ["alert"]
|
|
148
|
-
statuses << "no data" if options[:notify_no_data]
|
|
139
|
+
statuses << "no data" if options[:notify_no_data] || options[:on_missing_data] == "show_and_notify_no_data"
|
|
149
140
|
statuses << "warn" if options.dig(:thresholds, :warning)
|
|
150
141
|
options[:renotify_statuses] = statuses
|
|
151
142
|
end
|
|
152
143
|
|
|
153
|
-
# on_missing_data cannot be used with notify_no_data or no_data_timeframe
|
|
154
|
-
# TODO migrate everything to only use on_missing_data
|
|
155
|
-
if data.fetch(:type) == "event-v2 alert" || on_missing_data != "default"
|
|
156
|
-
options[:on_missing_data] = on_missing_data
|
|
157
|
-
options[:notify_no_data] = false # cannot set nil or it's an endless update loop
|
|
158
|
-
options.delete :no_data_timeframe
|
|
159
|
-
end
|
|
160
|
-
|
|
161
144
|
# only set when needed to avoid big diff
|
|
162
145
|
if (notification_preset_name = notification_preset_name())
|
|
163
146
|
options[:notification_preset_name] = notification_preset_name
|
|
164
147
|
end
|
|
165
148
|
|
|
166
|
-
# locked is deprecated, will fail if used
|
|
167
|
-
options.delete :locked
|
|
168
|
-
|
|
169
149
|
data
|
|
170
150
|
end
|
|
171
151
|
|
|
152
|
+
# TODO: migrate everything to only use on_missing_data by only sending notify_no_data when it was set by a user
|
|
153
|
+
# and enforce that it is not set at the same time as on_missing_data
|
|
154
|
+
def configure_no_data
|
|
155
|
+
notify = notify_no_data
|
|
156
|
+
timeframe = no_data_timeframe
|
|
157
|
+
action = on_missing_data
|
|
158
|
+
action ||= "default" if type == "event-v2 alert"
|
|
159
|
+
|
|
160
|
+
# TODO: mark setting action && !notify.nil? at all as invalid
|
|
161
|
+
if action
|
|
162
|
+
if ON_MISSING_DATA_UNSUPPORTED_TYPES.include?(type)
|
|
163
|
+
invalid! :invalid_no_data_config, "cannot use on_missing_data with #{type} monitor"
|
|
164
|
+
end
|
|
165
|
+
if timeframe
|
|
166
|
+
invalid! :invalid_no_data_config, "set either no_data_timeframe or on_missing_data"
|
|
167
|
+
end
|
|
168
|
+
if action != "default" && type == "query alert" && query.to_s.include?("default_zero(") # is allowed for log alert for example
|
|
169
|
+
invalid! :invalid_no_data_config, "set on_missing_data to `default` when using default_zero"
|
|
170
|
+
end
|
|
171
|
+
if action == "resolve" && timeout_h.to_i != 0
|
|
172
|
+
invalid! :invalid_no_data_config, "timeout_h cannot be set and non-zero when on_missing_data is `resolve`"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# on_missing_data cannot be used with notify_no_data + no_data_timeframe
|
|
177
|
+
if action
|
|
178
|
+
{ on_missing_data: action || "default" }
|
|
179
|
+
else
|
|
180
|
+
{
|
|
181
|
+
notify_no_data: notify,
|
|
182
|
+
no_data_timeframe: notify ? no_data_timeframe || default_no_data_timeframe : nil
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
172
187
|
def resolve_linked_tracking_ids!(id_map, **args)
|
|
173
188
|
case as_json[:type]
|
|
174
189
|
when "composite", "slo alert"
|
|
@@ -180,14 +195,32 @@ module Kennel
|
|
|
180
195
|
end
|
|
181
196
|
end
|
|
182
197
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if
|
|
187
|
-
|
|
198
|
+
# ensure type does not change, but not if it's metric->query which is supported and used by importer.rb
|
|
199
|
+
def allowed_update_error(actual)
|
|
200
|
+
actual_type = actual[:type]
|
|
201
|
+
return if actual_type == type || (actual_type == "metric alert" && type == "query alert")
|
|
202
|
+
"cannot update type from #{actual_type} to #{type}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# deprecated this setting is no longer returned by dd for new monitors
|
|
206
|
+
# datadog UI warns when setting no data timeframe to less than 2x the query window
|
|
207
|
+
# limited to 24h because `no_data_timeframe must not exceed group retention` and max group retention is 24h
|
|
208
|
+
def default_no_data_timeframe
|
|
209
|
+
default = 60
|
|
210
|
+
if type == "query alert" && (minutes = query_window_minutes)
|
|
211
|
+
(minutes * 2).clamp(default, 24 * 60)
|
|
212
|
+
else
|
|
213
|
+
default
|
|
188
214
|
end
|
|
189
215
|
end
|
|
190
216
|
|
|
217
|
+
# validate that monitors that alert on no data resolve in external services by using timeout_h, so it sends a
|
|
218
|
+
# notification when the no data group is removed from the monitor, which datadog does automatically after 24h
|
|
219
|
+
def timeout_h
|
|
220
|
+
sending_no_data_notifications = (on_missing_data ? on_missing_data == "show_and_notify_no_data" : notify_no_data)
|
|
221
|
+
sending_no_data_notifications ? 24 : MONITOR_OPTION_DEFAULTS.fetch(:timeout_h)
|
|
222
|
+
end
|
|
223
|
+
|
|
191
224
|
def self.api_resource
|
|
192
225
|
"monitor"
|
|
193
226
|
end
|
|
@@ -214,7 +247,9 @@ module Kennel
|
|
|
214
247
|
ignore_default(expected, actual, MONITOR_DEFAULTS)
|
|
215
248
|
|
|
216
249
|
options = actual.fetch(:options)
|
|
217
|
-
|
|
250
|
+
|
|
251
|
+
# we do not manage silenced: ignore it when diffing
|
|
252
|
+
options.delete(:silenced)
|
|
218
253
|
|
|
219
254
|
# fields are not returned when set to true
|
|
220
255
|
if ["service check", "event alert"].include?(actual[:type])
|
|
@@ -244,13 +279,38 @@ module Kennel
|
|
|
244
279
|
options.delete(:escalation_message)
|
|
245
280
|
expected_options.delete(:escalation_message)
|
|
246
281
|
end
|
|
282
|
+
|
|
247
283
|
# locked is deprecated: ignored when diffing
|
|
248
284
|
options.delete(:locked)
|
|
249
|
-
expected_options.delete(:locked)
|
|
250
285
|
end
|
|
251
286
|
|
|
252
287
|
private
|
|
253
288
|
|
|
289
|
+
def configure_thresholds
|
|
290
|
+
return {} if type == "composite"
|
|
291
|
+
|
|
292
|
+
thresholds = { critical: critical }
|
|
293
|
+
|
|
294
|
+
# set optional variables
|
|
295
|
+
[:warning, :ok, :critical_recovery, :warning_recovery].each do |key|
|
|
296
|
+
if (value = send(key))
|
|
297
|
+
thresholds[key] = value
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# custom logic for some types
|
|
302
|
+
case type
|
|
303
|
+
when "service check"
|
|
304
|
+
# avoid diff for default values of 1
|
|
305
|
+
OPTIONAL_SERVICE_CHECK_THRESHOLDS.each { |t| thresholds[t] ||= 1 }
|
|
306
|
+
when "query alert"
|
|
307
|
+
# metric and query values are stored as float by datadog
|
|
308
|
+
thresholds.each { |k, v| thresholds[k] = Float(v) }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
{ thresholds: thresholds }
|
|
312
|
+
end
|
|
313
|
+
|
|
254
314
|
def validate_json(data)
|
|
255
315
|
super
|
|
256
316
|
|
|
@@ -266,9 +326,11 @@ module Kennel
|
|
|
266
326
|
end
|
|
267
327
|
|
|
268
328
|
# verify query includes critical value
|
|
269
|
-
if (
|
|
270
|
-
if
|
|
271
|
-
|
|
329
|
+
if (critical = data.dig(:options, :thresholds, :critical))
|
|
330
|
+
if (query_value = data.fetch(:query)[/\s*[<>]=?\s*(\d+(\.\d+)?)\s*$/, 1])
|
|
331
|
+
if Float(query_value) != Float(critical)
|
|
332
|
+
invalid! :critical_does_not_match_query, "critical and value used in query must match"
|
|
333
|
+
end
|
|
272
334
|
end
|
|
273
335
|
end
|
|
274
336
|
|
|
@@ -297,7 +359,7 @@ module Kennel
|
|
|
297
359
|
message = data.fetch(:message)
|
|
298
360
|
|
|
299
361
|
used =
|
|
300
|
-
message.scan(/{{\s*(?:[#^]is(?:_exact)?_match)\s*"([^\s}]+)"/) + # {{#is_match "environment.name" "production"}}
|
|
362
|
+
message.scan(/{{\s*(?:[#^]is(?:_exact)?_match)\s*['"]([^\s}]+)['"]/) + # {{#is_match "environment.name" "production"}}
|
|
301
363
|
message.scan(/{{\s*([^}]+\.name)\s*}}/) # Pod {{pod.name}} failed
|
|
302
364
|
return if used.empty?
|
|
303
365
|
used.flatten!(1)
|
|
@@ -373,6 +435,11 @@ module Kennel
|
|
|
373
435
|
else # do nothing
|
|
374
436
|
end
|
|
375
437
|
end
|
|
438
|
+
|
|
439
|
+
def query_window_minutes
|
|
440
|
+
return unless (match = query.match(/^\s*\w+\(last_(?<count>\d+)(?<unit>[mhdw])\):/))
|
|
441
|
+
Integer(match["count"]) * MINUTES_PER_UNIT.fetch(match["unit"])
|
|
442
|
+
end
|
|
376
443
|
end
|
|
377
444
|
end
|
|
378
445
|
end
|
|
@@ -10,15 +10,21 @@ module Kennel
|
|
|
10
10
|
|
|
11
11
|
def self.file_location
|
|
12
12
|
return @file_location if defined?(@file_location)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
methods = instance_methods(false)
|
|
14
|
+
if methods.any?
|
|
15
|
+
@file_location = methods.detect do |method|
|
|
16
|
+
location = instance_method(method).source_location.first
|
|
17
|
+
if (path = find_relative_path(location))
|
|
18
|
+
break path
|
|
19
|
+
end
|
|
20
|
+
end || raise("Unable to find file_location for #{name}")
|
|
15
21
|
else
|
|
16
|
-
@file_location = nil
|
|
22
|
+
@file_location = nil # not sure if this is actually needed
|
|
17
23
|
end
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def validated_parts
|
|
21
|
-
all = parts
|
|
27
|
+
all = filter_parts(parts)
|
|
22
28
|
unless all.is_a?(Array) && all.all? { |part| part.is_a?(Record) }
|
|
23
29
|
raise "Project #{kennel_id} #parts must return an array of Records"
|
|
24
30
|
end
|
|
@@ -29,6 +35,16 @@ module Kennel
|
|
|
29
35
|
|
|
30
36
|
private
|
|
31
37
|
|
|
38
|
+
private_class_method def self.find_relative_path(path)
|
|
39
|
+
return path unless File.absolute_path?(path)
|
|
40
|
+
path.dup.sub!("#{Bundler.root}/", "") || path.dup.sub!("#{Dir.pwd}/", "")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# hook for users to add custom filtering via `prepend`
|
|
44
|
+
def filter_parts(parts)
|
|
45
|
+
parts
|
|
46
|
+
end
|
|
47
|
+
|
|
32
48
|
# hook for users to add custom validations via `prepend`
|
|
33
49
|
def validate_parts(parts)
|
|
34
50
|
end
|
data/lib/kennel/models/record.rb
CHANGED
|
@@ -142,12 +142,8 @@ module Kennel
|
|
|
142
142
|
@as_json
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def invalid_update!(field, old_value, new_value)
|
|
150
|
-
raise DisallowedUpdateError, "#{safe_tracking_id} Datadog does not allow update of #{field} (#{old_value.inspect} -> #{new_value.inspect})"
|
|
145
|
+
def allowed_update_error(_a)
|
|
146
|
+
nil
|
|
151
147
|
end
|
|
152
148
|
|
|
153
149
|
# For use during error handling
|
data/lib/kennel/models/slo.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Kennel
|
|
|
6
6
|
|
|
7
7
|
READONLY_ATTRIBUTES = [
|
|
8
8
|
*superclass::READONLY_ATTRIBUTES,
|
|
9
|
-
:type_id, :monitor_tags
|
|
9
|
+
:type_id, :monitor_tags
|
|
10
10
|
].freeze
|
|
11
11
|
TRACKING_FIELD = :description
|
|
12
12
|
DEFAULTS = {
|
|
@@ -14,10 +14,12 @@ module Kennel
|
|
|
14
14
|
query: nil,
|
|
15
15
|
groups: nil,
|
|
16
16
|
monitor_ids: [],
|
|
17
|
-
thresholds: []
|
|
17
|
+
thresholds: [],
|
|
18
|
+
primary: nil,
|
|
19
|
+
sli_specification: nil
|
|
18
20
|
}.freeze
|
|
19
21
|
|
|
20
|
-
settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name, :groups, :sli_specification
|
|
22
|
+
settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name, :groups, :sli_specification, :primary
|
|
21
23
|
|
|
22
24
|
defaults(
|
|
23
25
|
tags: -> { @project.tags },
|
|
@@ -25,7 +27,9 @@ module Kennel
|
|
|
25
27
|
description: -> { DEFAULTS.fetch(:description) },
|
|
26
28
|
monitor_ids: -> { DEFAULTS.fetch(:monitor_ids) },
|
|
27
29
|
thresholds: -> { DEFAULTS.fetch(:thresholds) },
|
|
28
|
-
groups: -> { DEFAULTS.fetch(:groups) }
|
|
30
|
+
groups: -> { DEFAULTS.fetch(:groups) },
|
|
31
|
+
primary: -> { DEFAULTS.fetch(:primary) },
|
|
32
|
+
sli_specification: -> { DEFAULTS.fetch(:sli_specification) }
|
|
29
33
|
)
|
|
30
34
|
|
|
31
35
|
def build_json
|
|
@@ -38,8 +42,18 @@ module Kennel
|
|
|
38
42
|
type: type
|
|
39
43
|
)
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
# add top level timeframe and threshold settings based on `primary`
|
|
46
|
+
if (p = primary)
|
|
47
|
+
data[:timeframe] = p
|
|
48
|
+
threshold =
|
|
49
|
+
thresholds.detect { |t| t[:timeframe] == p } ||
|
|
50
|
+
raise(ArgumentError, "#{tracking_id} unable to find threshold with timeframe #{p}")
|
|
51
|
+
data[:warning_threshold] = threshold[:warning]
|
|
52
|
+
data[:target_threshold] = threshold[:target]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if (v = sli_specification)
|
|
56
|
+
data[:sli_specification] = v
|
|
43
57
|
elsif (v = query)
|
|
44
58
|
data[:query] = v
|
|
45
59
|
end
|
|
@@ -60,7 +74,7 @@ module Kennel
|
|
|
60
74
|
end
|
|
61
75
|
|
|
62
76
|
def self.parse_url(url)
|
|
63
|
-
url[/[?&]slo_id=([a-z\d]{10,})/, 1] || url[/\/slo\/([a-z\d]{10,})
|
|
77
|
+
url[/[?&]slo_id=([a-z\d]{10,})/, 1] || url[/\/slo\/([a-z\d]{10,})(:?\/edit)?(\?|$)/, 1]
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
def resolve_linked_tracking_ids!(id_map, **args)
|
|
@@ -83,6 +97,21 @@ module Kennel
|
|
|
83
97
|
expected[:tags]&.sort!
|
|
84
98
|
actual[:tags].sort!
|
|
85
99
|
|
|
100
|
+
# do not show these in the diff if we automatically pick the primary timeframe,
|
|
101
|
+
# or we will have a permanent `something -> nil` diff
|
|
102
|
+
unless expected[:timeframe]
|
|
103
|
+
[:timeframe, :warning_threshold, :target_threshold].each { |k| actual.delete k }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# discard deprecated query which stays in datadog forever when we are trying to set sli_specification
|
|
107
|
+
# (downgrading to query by setting sli_specification=nil is not supported in the api)
|
|
108
|
+
actual.delete :query if expected[:sli_specification]
|
|
109
|
+
|
|
110
|
+
# user set query so let's not worry about sli_specification even if this might be hiding bugs
|
|
111
|
+
# ideally we'd validate and tell the user that this will have no effect (see importer logic)
|
|
112
|
+
# but I'm not confident this will always be right
|
|
113
|
+
actual.delete :sli_specification if expected[:query]
|
|
114
|
+
|
|
86
115
|
ignore_default(expected, actual, DEFAULTS)
|
|
87
116
|
end
|
|
88
117
|
|
|
@@ -97,6 +126,11 @@ module Kennel
|
|
|
97
126
|
invalid! :tags_are_upper_case, "Tags must not be upper case (bad tags: #{bad_tags.sort.inspect})"
|
|
98
127
|
end
|
|
99
128
|
|
|
129
|
+
# Check that thresholds are not empty
|
|
130
|
+
if !data[:thresholds] || data[:thresholds].empty?
|
|
131
|
+
invalid! :thresholds_empty, "SLO must have at least one threshold defined"
|
|
132
|
+
end
|
|
133
|
+
|
|
100
134
|
# prevent "Invalid payload: The target is incorrect: target must be a positive number between (0.0, 100.0)"
|
|
101
135
|
data[:thresholds]&.each do |threshold|
|
|
102
136
|
target = threshold.fetch(:target)
|
|
@@ -54,6 +54,9 @@ module Kennel
|
|
|
54
54
|
actual[:locations] = actual[:locations]&.sort
|
|
55
55
|
|
|
56
56
|
ignore_default(expected, actual, DEFAULTS)
|
|
57
|
+
|
|
58
|
+
# only update downtime_ids if we are trying to manage them
|
|
59
|
+
actual[:options]&.delete :downtime_ids unless expected.dig(:options, :downtime_ids)
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
end
|
data/lib/kennel/models/team.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module Kennel
|
|
3
3
|
module Models
|
|
4
|
-
class Team
|
|
5
|
-
|
|
4
|
+
class Team
|
|
5
|
+
include SettingsAsMethods
|
|
6
|
+
|
|
7
|
+
settings :mention, :tags, :renotify_interval
|
|
6
8
|
defaults(
|
|
7
|
-
tags: -> { ["team:#{
|
|
9
|
+
tags: -> { ["team:#{StringUtils.snake_case(self.class.name).sub(/^teams_/, "").tr("_", "-")}"] },
|
|
8
10
|
renotify_interval: -> { 0 }
|
|
9
11
|
)
|
|
10
12
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Kennel
|
|
4
4
|
class PartsSerializer
|
|
5
|
+
FILE_EXTENSION = ".json"
|
|
6
|
+
FOLDER = "generated"
|
|
7
|
+
|
|
5
8
|
def initialize(filter:)
|
|
6
9
|
@filter = filter
|
|
7
10
|
end
|
|
@@ -9,8 +12,15 @@ module Kennel
|
|
|
9
12
|
def write(parts)
|
|
10
13
|
Progress.progress "Storing" do
|
|
11
14
|
existing = existing_files_and_folders
|
|
12
|
-
used = write_changed(parts)
|
|
13
|
-
FileUtils.rm_rf(existing - used)
|
|
15
|
+
used, changed = write_changed(parts)
|
|
16
|
+
FileUtils.rm_rf(existing - used) # cleanup abandoned
|
|
17
|
+
suggest_using_project_filter(changed)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def tracking_id_for_path(path)
|
|
23
|
+
path.sub("#{FOLDER}/", "").sub(FILE_EXTENSION, "").sub("/", ":")
|
|
14
24
|
end
|
|
15
25
|
end
|
|
16
26
|
|
|
@@ -20,28 +30,29 @@ module Kennel
|
|
|
20
30
|
|
|
21
31
|
def write_changed(parts)
|
|
22
32
|
used = []
|
|
33
|
+
changed = []
|
|
23
34
|
|
|
24
35
|
Utils.parallel(parts, max: 2) do |part|
|
|
25
36
|
path = path_for_tracking_id(part.tracking_id)
|
|
26
37
|
|
|
38
|
+
# match paths returned from existing_files_and_folders
|
|
27
39
|
used << File.dirname(path) # we have 1 level of sub folders, so this is enough
|
|
28
40
|
used << path
|
|
29
41
|
|
|
30
42
|
content = part.as_json.merge(api_resource: part.class.api_resource)
|
|
31
|
-
write_file_if_necessary(path, content)
|
|
43
|
+
changed << path if write_file_if_necessary(path, content)
|
|
32
44
|
end
|
|
33
|
-
|
|
34
|
-
used
|
|
45
|
+
[used, changed]
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
def existing_files_and_folders
|
|
38
|
-
paths = Dir["
|
|
49
|
+
paths = Dir["#{FOLDER}/**/*"] # we rely on this returning folders and files, see write_changed
|
|
39
50
|
|
|
40
51
|
# when filtering we only need the files we are going to write
|
|
41
52
|
if filter.filtering?
|
|
42
53
|
paths.select! do |path|
|
|
43
|
-
tracking_id =
|
|
44
|
-
filter.
|
|
54
|
+
tracking_id = self.class.tracking_id_for_path(path)
|
|
55
|
+
filter.filters_tracking_id?(tracking_id)
|
|
45
56
|
end
|
|
46
57
|
end
|
|
47
58
|
|
|
@@ -49,7 +60,7 @@ module Kennel
|
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
def path_for_tracking_id(tracking_id)
|
|
52
|
-
"
|
|
63
|
+
"#{FOLDER}/#{tracking_id.tr("/", ":").sub(":", "/")}#{FILE_EXTENSION}"
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
def write_file_if_necessary(path, content)
|
|
@@ -58,13 +69,21 @@ module Kennel
|
|
|
58
69
|
|
|
59
70
|
# 99% case
|
|
60
71
|
begin
|
|
61
|
-
return if File.read(path) == content
|
|
62
|
-
rescue Errno::ENOENT
|
|
72
|
+
return false if File.read(path) == content
|
|
73
|
+
rescue Errno::ENOENT # file or even folder did not exist
|
|
63
74
|
FileUtils.mkdir_p(File.dirname(path))
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
# slow 1% case
|
|
67
78
|
File.write(path, content)
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def suggest_using_project_filter(changed)
|
|
83
|
+
return if filter.filtering?
|
|
84
|
+
projects = changed.map { |path| path.split("/")[1] }.uniq
|
|
85
|
+
return if projects.size != 1
|
|
86
|
+
warn "Hint: Using PROJECT=#{projects[0]} is faster"
|
|
68
87
|
end
|
|
69
88
|
end
|
|
70
89
|
end
|
|
@@ -12,13 +12,14 @@ module Kennel
|
|
|
12
12
|
# All requested projects. This is a slow operation when loading all projects.
|
|
13
13
|
def projects
|
|
14
14
|
load_requested
|
|
15
|
-
loaded_projects.map(&:new)
|
|
15
|
+
projects = loaded_projects.map(&:new)
|
|
16
|
+
@filter.filter_projects projects # in case we loaded more though dependencies
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
private
|
|
19
20
|
|
|
20
21
|
def loaded_projects
|
|
21
|
-
Models::Project.recursive_subclasses
|
|
22
|
+
Models::Project.recursive_subclasses.reject(&:abstract_class?)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# "require" requested .rb files under './projects',
|
|
@@ -9,8 +9,16 @@ module Kennel
|
|
|
9
9
|
@subclasses ||= []
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def abstract_class?
|
|
13
|
+
!!@abstract_class
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
private
|
|
13
17
|
|
|
18
|
+
def abstract_class!
|
|
19
|
+
@abstract_class = true # not inherited by children
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def inherited(child)
|
|
15
23
|
super
|
|
16
24
|
subclasses << child
|
|
@@ -36,7 +36,13 @@ module Kennel
|
|
|
36
36
|
|
|
37
37
|
def matching_expected(a, map)
|
|
38
38
|
klass = a.fetch(:klass)
|
|
39
|
-
|
|
39
|
+
full_id = "#{klass.api_resource}:#{a.fetch(:id)}"
|
|
40
|
+
if (e = map[full_id]) # we try to update and the user has set the id
|
|
41
|
+
return e unless (error = e.allowed_update_error(a))
|
|
42
|
+
raise DisallowedUpdateError, "#{full_id} Datadog does not allow update: #{error}"
|
|
43
|
+
elsif (e = map[a.fetch(:tracking_id)])
|
|
44
|
+
e.allowed_update_error(a) ? nil : e # force a re-create if we can't update
|
|
45
|
+
end
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
end
|
|
@@ -12,9 +12,9 @@ module Kennel
|
|
|
12
12
|
if plan.empty?
|
|
13
13
|
Kennel.out.puts Console.color(:green, "Nothing to do")
|
|
14
14
|
else
|
|
15
|
+
print_changes "Delete", plan.deletes, :red
|
|
15
16
|
print_changes "Create", plan.creates, :green
|
|
16
17
|
print_changes "Update", plan.updates, :yellow
|
|
17
|
-
print_changes "Delete", plan.deletes, :red
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ module Kennel
|
|
|
27
27
|
# ignore when deleted from the codebase
|
|
28
28
|
# (when running with filters we cannot see the other resources in the codebase)
|
|
29
29
|
api_resource = a.fetch(:klass).api_resource
|
|
30
|
-
next if !id_map.get(api_resource, tracking_id) && filter.
|
|
30
|
+
next if !id_map.get(api_resource, tracking_id) && filter.filters_tracking_id?(tracking_id)
|
|
31
31
|
|
|
32
32
|
id_map.set(api_resource, tracking_id, a.fetch(:id))
|
|
33
33
|
if a.fetch(:klass).api_resource == "synthetics/tests"
|