kennel 2.19.0 → 2.20.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: 310e71677ad8a948d4518ddd49216ac0ca3a9f6e81be46b1f5ad890deb5cf262
4
- data.tar.gz: b4e2efe18a029b6cc2340728da6993c53e74b2d84c689805726deeaa4822d7ba
3
+ metadata.gz: d41a632f2e488aacff6253cdb1b161d628d7522ffd4b7ce3e006455b912ca16d
4
+ data.tar.gz: 0ffe3c8a9ec7d828aa397460af8f06af307f14e4a9740ff0e1a1693bc6c65950
5
5
  SHA512:
6
- metadata.gz: e86edb395863a4e8d6a0a5ccbb0cb1acf514326a0168def890a184d32ca834273256dd4f2cf38112eaecd49d4373d97fa575c067ddd6d9d9addab50a7694fa72
7
- data.tar.gz: 15f6cc720bd886aa477b24d3bc70d0327696e6c0a3e6727cd609a78681a65907dbab89b743e455076e91c6a1ad8d5115f090a3a6e277438693f24f8538b03164
6
+ metadata.gz: 8151f10629556f4b9dbad8b11c69e032fca8ebbd00a50e888482a2bb37659ce70028a8740606a90745de3b3808967165b4cc10c1faeac2ccce707bfc57cb8811
7
+ data.tar.gz: 6682f0c4815aacd911464f92c94f376110e52219c465f6f482dfbb85399b0ec4ea2b5d33d78ae394f753a04ef959b6b766325bd4bbc2f0b461dd172a24ed1349
data/Readme.md CHANGED
@@ -444,6 +444,9 @@ Run `rake kennel:alerts TAG=service:my-service` to see all un-muted alerts for a
444
444
  Use `KNOWN=foo@bar.com,baz@bar.com` to exempt mentions that are not returned by the API.
445
445
  Use `KNOWN_RANDOM=@sns-foo,@sns-bar` to ignore for example SNS handles that are randomly invalid in the API.
446
446
 
447
+ ### Validating planned changes
448
+ `rake kennel:validate_plan` validates planned monitor and dashboard changes against the Datadog API.
449
+
447
450
  ### Grepping through all of datadog
448
451
  ```Bash
449
452
  rake kennel:dump > tmp/dump
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Dump ALL of datadog config as raw json ... useful for grep/search [TYPE=slo|monitor|dashboard]"
5
+ task dump: :environment do
6
+ resources =
7
+ if (type = ENV["TYPE"])
8
+ [type]
9
+ else
10
+ Kennel::Models::Record.api_resource_map.keys
11
+ end
12
+ api = Kennel::Api.new
13
+ list = nil
14
+ first = true
15
+
16
+ Kennel.out.puts "["
17
+ resources.each do |resource|
18
+ Kennel::Progress.progress("Downloading #{resource}") do
19
+ list = api.list(resource)
20
+ api.fill_details!(resource, list) if resource == "dashboard"
21
+ end
22
+ list.each do |r|
23
+ r[:api_resource] = resource
24
+ if first
25
+ first = false
26
+ else
27
+ Kennel.out.puts ","
28
+ end
29
+ Kennel.out.print JSON.pretty_generate(r)
30
+ end
31
+ end
32
+ Kennel.out.puts "\n]"
33
+ end
34
+
35
+ desc "Find items from dump by pattern DUMP= PATTERN= [URLS=true]"
36
+ task dump_grep: :environment do
37
+ file = ENV.fetch("DUMP")
38
+ pattern = Regexp.new ENV.fetch("PATTERN")
39
+ items = File.read(file)[2..-2].gsub("},\n{", "}--SPLIT--{").split("--SPLIT--")
40
+ models = Kennel::Models::Record.api_resource_map
41
+ found = items.grep(pattern)
42
+ exit 1 if found.empty?
43
+ found.each do |resource|
44
+ if ENV["URLS"]
45
+ parsed = JSON.parse(resource)
46
+ url = models[parsed.fetch("api_resource")].url(parsed.fetch("id"))
47
+ title = parsed["title"] || parsed["name"]
48
+ Kennel.out.puts "#{url} # #{title}"
49
+ else
50
+ Kennel.out.puts resource
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Convert existing resources to copy-pasteable definitions to import existing resources (call with URL= or call with RESOURCE= and ID=)"
5
+ task import: :environment do
6
+ if (id = ENV["ID"]) && (resource = ENV["RESOURCE"])
7
+ id = Integer(id) if id =~ /^\d+$/
8
+ elsif (url = ENV["URL"])
9
+ resource, id = Kennel::Models::Record.parse_any_url(url) || Kennel::Tasks.abort("Unable to parse url")
10
+ else
11
+ possible_resources = Kennel::Models::Record.subclasses.map(&:api_resource)
12
+ Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
13
+ end
14
+
15
+ Kennel.out.puts Kennel::Importer.new(Kennel::Api.new).import(resource, id)
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "show monitors with no data by TAG, for example TAG=team:foo [THRESHOLD_DAYS=7] [FORMAT=json]"
5
+ task nodata: :environment do
6
+ tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
7
+ monitors = Kennel::Api.new.list("monitor", monitor_tags: tag, group_states: "no data")
8
+ monitors.select! { |m| m[:overall_state] == "No Data" }
9
+ monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
10
+ if monitors.any?
11
+ Kennel.err.puts <<~TEXT
12
+ To ignore monitors with expected nodata, tag it with "nodata:ignore"
13
+
14
+ TEXT
15
+ end
16
+
17
+ now = Time.now
18
+ monitors.each do |m|
19
+ m[:days_in_no_data] =
20
+ if m[:overall_state_modified]
21
+ since = Date.parse(m[:overall_state_modified]).to_time
22
+ ((now - since) / (24 * 60 * 60)).to_i
23
+ else
24
+ 999
25
+ end
26
+ end
27
+
28
+ if (threshold = ENV["THRESHOLD_DAYS"])
29
+ monitors.select! { |m| m[:days_in_no_data] > Integer(threshold) }
30
+ end
31
+
32
+ monitors.each { |m| m[:url] = Kennel::Utils.path_to_url("/monitors/#{m[:id]}") }
33
+
34
+ if ENV["FORMAT"] == "json"
35
+ report = monitors.map do |m|
36
+ match = m[:message].to_s.match(/-- #{Regexp.escape(Kennel::Models::Record::MARKER_TEXT)} (\S+:\S+) in (\S+), /) || []
37
+ m.slice(:url, :name, :tags, :days_in_no_data).merge(
38
+ kennel_tracking_id: match[1],
39
+ kennel_source: match[2]
40
+ )
41
+ end
42
+
43
+ Kennel.out.puts JSON.pretty_generate(report)
44
+ else
45
+ monitors.each do |m|
46
+ Kennel.out.puts m[:name]
47
+ Kennel.out.puts Kennel::Utils.path_to_url("/monitors/#{m[:id]}")
48
+ Kennel.out.puts "No data since #{m[:days_in_no_data]}d"
49
+ Kennel.out.puts
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Resolve given id to kennel tracking-id RESOURCE= ID="
5
+ task tracking_id: "kennel:environment" do
6
+ resource = ENV.fetch("RESOURCE")
7
+ id = ENV.fetch("ID")
8
+ klass =
9
+ Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
10
+ raise("resource #{resource} not know")
11
+ object = Kennel::Api.new.show(resource, id)
12
+ Kennel.out.puts klass.parse_tracking_id(object)
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Verify that all used monitor mentions are valid"
5
+ task validate_mentions: :environment do
6
+ known = []
7
+
8
+ # @slack- @team- @webhook- @sns- user-emails
9
+ known += Kennel::Api.new.send(:request, :get, "/api/v2/notifications/handles?group_limit=99999")
10
+ .fetch(:data)
11
+ .flat_map { |d| d.dig(:attributes, :handles) }
12
+ .map { |v| v.fetch(:value) }
13
+
14
+ # group emails or other 1-off things we know are valid
15
+ manual = ENV["KNOWN"].to_s.split(",")
16
+ dupes = (manual & known)
17
+ Kennel::Tasks.abort "KNOWN=#{dupes.join(",")} values are already known and should be removed" if dupes.any?
18
+ known += manual
19
+
20
+ # @sns- handles are randomly invalid so we need to ignore them without checking if the ignore is needed
21
+ # https://help.datadoghq.com/hc/en-us/requests/2310423
22
+ known += ENV["KNOWN_RANDOM"].to_s.split(",")
23
+
24
+ bad = []
25
+ Dir["generated/**/*.json"].each do |f|
26
+ next unless (message = JSON.parse(File.read(f))["message"])
27
+ used = message
28
+ .scan(/(?:^|\s)(@[^\s{,'"]+)/)
29
+ .flatten(1)
30
+ .grep(/^@.*@|^@.*-/) # ignore @here etc handles ... datadog uses @foo@bar.com for emails and @foo-bar for integrations
31
+ (used - known).each { |v| bad << [f, v] }
32
+ end
33
+
34
+ if bad.any?
35
+ url = Kennel::Utils.path_to_url "/account/settings"
36
+ Kennel.err.puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
37
+ bad.each { |f, v| Kennel.err.puts "Invalid mention #{v} in monitor message of #{f}" }
38
+ Kennel::Tasks.abort ENV["KNOWN_WARNING"]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ module ValidatePlan
4
+ class MonitorValidator
5
+ COSMETIC_FIELDS = ["name", "message", "tags"].freeze
6
+
7
+ def initialize(item)
8
+ @item = item
9
+ end
10
+
11
+ def validate(api)
12
+ data = @item.expected.as_json
13
+
14
+ # ignore unresolved ids from yet to be created monitors
15
+ return nil if ["composite", "slo alert"].include?(data[:type]) && data[:query].include?("%")
16
+
17
+ api.send(:request, :post, "/api/v1/monitor/validate", body: data)
18
+ nil
19
+ rescue StandardError => e
20
+ "#{Kennel::Console.color(:yellow, "#{@item.api_resource} #{@item.tracking_id}:")}\n#{e.message}"
21
+ end
22
+ end
23
+
24
+ class DashboardValidator
25
+ COSMETIC_FIELDS = ["title", "description", "tags"].freeze
26
+
27
+ def initialize(item)
28
+ @item = item
29
+ end
30
+
31
+ # datadog does not offer a validation api for dashboards,
32
+ # so we insert an invalid widget at the end and see if that is the invalid widget it complains about
33
+ # this will break if they ever start from the back or return errors for everything that is invalid
34
+ #
35
+ # we do not need to worry about unresolved ids because:
36
+ # - alert_graph widgets allows kennel style ids
37
+ # - slo widgets allow kennel style ids
38
+ # - uptime widgets allow kennel style ids
39
+ def validate(api)
40
+ json = @item.expected.as_json
41
+ json = Marshal.load(Marshal.dump(json))
42
+
43
+ # add a semi-valid (does not fail immediately on missing definition) widget still blocks the request
44
+ placeholder = "invalid_metric_do_not_update"
45
+ json.fetch(:widgets) << {
46
+ definition: {
47
+ type: "timeseries", requests: [{
48
+ response_format: "timeseries",
49
+ queries: [{ data_source: "metrics", name: "restarts", query: placeholder }]
50
+ }]
51
+ },
52
+ layout: { x: 0, y: 0, height: 0, width: 0 } # needed for `layout_type: free` and valid for all
53
+ }
54
+
55
+ begin
56
+ if @item.class::TYPE == :update
57
+ api.update("dashboard", @item.actual.fetch(:id), json)
58
+ else
59
+ api.create("dashboard", json)
60
+ end
61
+ raise "Dashboard validation should have failed, live dashboard was update/created by accident"
62
+ rescue StandardError => e
63
+ # parse the JSON in the error message and see if there is anything except our error
64
+ raise "Unreadable error format: #{e.message}" unless (json = e.message[/^\{"errors":.*}$/m])
65
+ data =
66
+ begin
67
+ JSON.parse(json)
68
+ rescue JSON::ParserError
69
+ raise "Unreadable error format: #{json}"
70
+ end
71
+ raise "Unreadable error format: #{data}" unless (errors = data["errors"]) # uncovered
72
+ return if errors.size == 1 && errors.all? { |m| m.include?("unable to parse #{placeholder}") }
73
+ "#{@item.tracking_id}: #{e.message}"
74
+ end
75
+ end
76
+ end
77
+
78
+ VALIDATORS = {
79
+ "monitor" => MonitorValidator,
80
+ "dashboard" => DashboardValidator
81
+ }.freeze
82
+
83
+ def self.validate(plan)
84
+ changes = (plan.creates + plan.updates)
85
+
86
+ validators = changes.filter_map do |item|
87
+ next unless (validator = VALIDATORS[item.api_resource]&.new(item))
88
+
89
+ if item.class::TYPE == :update
90
+ # ignore if nothing can break
91
+ modified_fields = item.diff.map { |_, f, *| f }
92
+ next nil if modified_fields.all? { |f| validator.class::COSMETIC_FIELDS.include?(f) }
93
+ end
94
+
95
+ validator
96
+ end
97
+
98
+ api = Kennel::Api.new
99
+ errors = validators.filter_map { |v| v.validate(api) }
100
+ return if errors.empty?
101
+
102
+ abort "#{Kennel::Console.color(:red, "#{errors.size} validation(s) failed:")}\n#{errors.join("\n")}"
103
+ end
104
+ end
105
+ end
106
+
107
+ namespace :kennel do
108
+ desc "Validate planned changes against the Datadog API [PROJECT=]"
109
+ task "validate_plan" => "kennel:environment" do
110
+ kennel = Kennel::Tasks.kennel
111
+ kennel.preload
112
+ Kennel::ValidatePlan.validate(kennel.plan)
113
+ end
114
+ end
data/lib/kennel/tasks.rb CHANGED
@@ -5,6 +5,8 @@ require "kennel/unmuted_alerts"
5
5
  require "kennel/importer"
6
6
  require "json"
7
7
 
8
+ Dir.children("#{__dir__}/tasks").each { |f| require_relative "tasks/#{File.basename(f, ".rb")}" }
9
+
8
10
  module Kennel
9
11
  module Tasks
10
12
  class << self
@@ -49,7 +51,11 @@ module Kennel
49
51
 
50
52
  def on_default_branch?
51
53
  branch = (ENV["TRAVIS_BRANCH"] || ENV["GITHUB_REF"]).to_s.sub(/^refs\/heads\//, "")
52
- (branch == (ENV["DEFAULT_BRANCH"] || "master"))
54
+ if (default = ENV["DEFAULT_BRANCH"])
55
+ branch == default
56
+ else
57
+ ["main", "master"].include?(branch)
58
+ end
53
59
  end
54
60
 
55
61
  def git_push?
@@ -67,49 +73,6 @@ namespace :kennel do
67
73
  Kennel::Tasks.abort "Error during diffing" unless $CHILD_STATUS.success?
68
74
  end
69
75
 
70
- # ideally do this on every run, but it's slow (~1.5s) and brittle
71
- # (might not find all via the regex + might find false-positives because random emails can also be sent to)
72
- # https://help.datadoghq.com/hc/en-us/requests/254114 for automatic validation
73
- # /monitor/notifications has users+slack+sns but not @team- and @webhook-
74
- # got a support ticket open to get sns into api/v2 too
75
- desc "Verify that all used monitor mentions are valid"
76
- task validate_mentions: :environment do
77
- known = []
78
-
79
- # @slack- @team- @webhook- @sns- user-emails
80
- known += Kennel::Api.new.send(:request, :get, "/api/v2/notifications/handles?group_limit=99999")
81
- .fetch(:data)
82
- .flat_map { |d| d.dig(:attributes, :handles) }
83
- .map { |v| v.fetch(:value) }
84
-
85
- # group emails or other 1-off things we know are valid
86
- manual = ENV["KNOWN"].to_s.split(",")
87
- dupes = (manual & known)
88
- Kennel::Tasks.abort "KNOWN=#{dupes.join(",")} values are already known and should be removed" if dupes.any?
89
- known += manual
90
-
91
- # @sns- handles are randomly invalid so we need to ignore them without checking if the ignore is needed
92
- # https://help.datadoghq.com/hc/en-us/requests/2310423
93
- known += ENV["KNOWN_RANDOM"].to_s.split(",")
94
-
95
- bad = []
96
- Dir["generated/**/*.json"].each do |f|
97
- next unless (message = JSON.parse(File.read(f))["message"])
98
- used = message
99
- .scan(/(?:^|\s)(@[^\s{,'"]+)/)
100
- .flatten(1)
101
- .grep(/^@.*@|^@.*-/) # ignore @here etc handles ... datadog uses @foo@bar.com for emails and @foo-bar for integrations
102
- (used - known).each { |v| bad << [f, v] }
103
- end
104
-
105
- if bad.any?
106
- url = Kennel::Utils.path_to_url "/account/settings"
107
- Kennel.err.puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
108
- bad.each { |f, v| Kennel.err.puts "Invalid mention #{v} in monitor message of #{f}" }
109
- Kennel::Tasks.abort ENV["KNOWN_WARNING"]
110
- end
111
- end
112
-
113
76
  desc "store definitions in generated/"
114
77
  task generate: :environment do
115
78
  Kennel::Tasks.kennel.generate
@@ -142,132 +105,6 @@ namespace :kennel do
142
105
  Kennel::UnmutedAlerts.print(Kennel::Api.new, tag)
143
106
  end
144
107
 
145
- desc "show monitors with no data by TAG, for example TAG=team:foo [THRESHOLD_DAYS=7] [FORMAT=json]"
146
- task nodata: :environment do
147
- tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
148
- monitors = Kennel::Api.new.list("monitor", monitor_tags: tag, group_states: "no data")
149
- monitors.select! { |m| m[:overall_state] == "No Data" }
150
- monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
151
- if monitors.any?
152
- Kennel.err.puts <<~TEXT
153
- To ignore monitors with expected nodata, tag it with "nodata:ignore"
154
-
155
- TEXT
156
- end
157
-
158
- now = Time.now
159
- monitors.each do |m|
160
- m[:days_in_no_data] =
161
- if m[:overall_state_modified]
162
- since = Date.parse(m[:overall_state_modified]).to_time
163
- ((now - since) / (24 * 60 * 60)).to_i
164
- else
165
- 999
166
- end
167
- end
168
-
169
- if (threshold = ENV["THRESHOLD_DAYS"])
170
- monitors.select! { |m| m[:days_in_no_data] > Integer(threshold) }
171
- end
172
-
173
- monitors.each { |m| m[:url] = Kennel::Utils.path_to_url("/monitors/#{m[:id]}") }
174
-
175
- if ENV["FORMAT"] == "json"
176
- report = monitors.map do |m|
177
- match = m[:message].to_s.match(/-- #{Regexp.escape(Kennel::Models::Record::MARKER_TEXT)} (\S+:\S+) in (\S+), /) || []
178
- m.slice(:url, :name, :tags, :days_in_no_data).merge(
179
- kennel_tracking_id: match[1],
180
- kennel_source: match[2]
181
- )
182
- end
183
-
184
- Kennel.out.puts JSON.pretty_generate(report)
185
- else
186
- monitors.each do |m|
187
- Kennel.out.puts m[:name]
188
- Kennel.out.puts Kennel::Utils.path_to_url("/monitors/#{m[:id]}")
189
- Kennel.out.puts "No data since #{m[:days_in_no_data]}d"
190
- Kennel.out.puts
191
- end
192
- end
193
- end
194
-
195
- desc "Convert existing resources to copy-pasteable definitions to import existing resources (call with URL= or call with RESOURCE= and ID=)"
196
- task import: :environment do
197
- if (id = ENV["ID"]) && (resource = ENV["RESOURCE"])
198
- id = Integer(id) if id =~ /^\d+$/ # dashboards can have alphanumeric ids
199
- elsif (url = ENV["URL"])
200
- resource, id = Kennel::Models::Record.parse_any_url(url) || Kennel::Tasks.abort("Unable to parse url")
201
- else
202
- possible_resources = Kennel::Models::Record.subclasses.map(&:api_resource)
203
- Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
204
- end
205
-
206
- Kennel.out.puts Kennel::Importer.new(Kennel::Api.new).import(resource, id)
207
- end
208
-
209
- desc "Dump ALL of datadog config as raw json ... useful for grep/search [TYPE=slo|monitor|dashboard]"
210
- task dump: :environment do
211
- resources =
212
- if (type = ENV["TYPE"])
213
- [type]
214
- else
215
- Kennel::Models::Record.api_resource_map.keys
216
- end
217
- api = Kennel::Api.new
218
- list = nil
219
- first = true
220
-
221
- Kennel.out.puts "["
222
- resources.each do |resource|
223
- Kennel::Progress.progress("Downloading #{resource}") do
224
- list = api.list(resource)
225
- api.fill_details!(resource, list) if resource == "dashboard"
226
- end
227
- list.each do |r|
228
- r[:api_resource] = resource
229
- if first
230
- first = false
231
- else
232
- Kennel.out.puts ","
233
- end
234
- Kennel.out.print JSON.pretty_generate(r)
235
- end
236
- end
237
- Kennel.out.puts "\n]"
238
- end
239
-
240
- desc "Find items from dump by pattern DUMP= PATTERN= [URLS=true]"
241
- task dump_grep: :environment do
242
- file = ENV.fetch("DUMP")
243
- pattern = Regexp.new ENV.fetch("PATTERN")
244
- items = File.read(file)[2..-2].gsub("},\n{", "}--SPLIT--{").split("--SPLIT--")
245
- models = Kennel::Models::Record.api_resource_map
246
- found = items.grep(pattern)
247
- exit 1 if found.empty?
248
- found.each do |resource|
249
- if ENV["URLS"]
250
- parsed = JSON.parse(resource)
251
- url = models[parsed.fetch("api_resource")].url(parsed.fetch("id"))
252
- title = parsed["title"] || parsed["name"]
253
- Kennel.out.puts "#{url} # #{title}"
254
- else
255
- Kennel.out.puts resource
256
- end
257
- end
258
- end
259
-
260
- desc "Resolve given id to kennel tracking-id RESOURCE= ID="
261
- task tracking_id: "kennel:environment" do
262
- resource = ENV.fetch("RESOURCE")
263
- id = ENV.fetch("ID")
264
- klass =
265
- Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
266
- raise("resource #{resource} not know")
267
- object = Kennel::Api.new.show(resource, id)
268
- Kennel.out.puts klass.parse_tracking_id(object)
269
- end
270
-
271
108
  task :environment do
272
109
  Kennel::Tasks.load_environment
273
110
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "2.19.0"
3
+ VERSION = "2.20.0"
4
4
  end
data/template/Readme.md CHANGED
@@ -426,6 +426,9 @@ Run `rake kennel:alerts TAG=service:my-service` to see all un-muted alerts for a
426
426
  Use `KNOWN=foo@bar.com,baz@bar.com` to exempt mentions that are not returned by the API.
427
427
  Use `KNOWN_RANDOM=@sns-foo,@sns-bar` to ignore for example SNS handles that are randomly invalid in the API.
428
428
 
429
+ ### Validating planned changes
430
+ `rake kennel:validate_plan` validates planned monitor and dashboard changes against the Datadog API.
431
+
429
432
  ### Grepping through all of datadog
430
433
  ```Bash
431
434
  rake kennel:dump > tmp/dump
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kennel
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.19.0
4
+ version: 2.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
@@ -117,6 +117,12 @@ files:
117
117
  - lib/kennel/syncer/types.rb
118
118
  - lib/kennel/tags_validation.rb
119
119
  - lib/kennel/tasks.rb
120
+ - lib/kennel/tasks/dump.rb
121
+ - lib/kennel/tasks/import.rb
122
+ - lib/kennel/tasks/nodata.rb
123
+ - lib/kennel/tasks/tracking_id.rb
124
+ - lib/kennel/tasks/validate_mentions.rb
125
+ - lib/kennel/tasks/validate_plan.rb
120
126
  - lib/kennel/template_variables.rb
121
127
  - lib/kennel/unmuted_alerts.rb
122
128
  - lib/kennel/utils.rb