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 +4 -4
- data/Readme.md +3 -0
- 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 +7 -170
- data/lib/kennel/version.rb +1 -1
- data/template/Readme.md +3 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d41a632f2e488aacff6253cdb1b161d628d7522ffd4b7ce3e006455b912ca16d
|
|
4
|
+
data.tar.gz: 0ffe3c8a9ec7d828aa397460af8f06af307f14e4a9740ff0e1a1693bc6c65950
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
(
|
|
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
|
data/lib/kennel/version.rb
CHANGED
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.
|
|
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
|