kennel 1.55.0 → 1.57.2

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: adc230a06e4f7f969147e0c299ba61c4536d5623de432cfe3e247bece944962a
4
- data.tar.gz: ae6d3ebe5db8fe7a5f406cda15e0a85d8a19b8d5f991a9787a63376a01572e70
3
+ metadata.gz: edff257eab30fa336ef8a067c7d02a5c4cfdb919b2381cbb88593e7292f4bef7
4
+ data.tar.gz: d166cc4f9bba2c73edf292e5d2a6e1e4e190125a04b608594b0e78055755134c
5
5
  SHA512:
6
- metadata.gz: 3013cb61dd5c33a7dd1772b54645d62e7cf7e70bb5202f85950ab4f2a102e723d1583725200da513a54d66020af14ca9c57b63bc50e43b1a27a4d765d478c2fb
7
- data.tar.gz: 28f5812c2795043927ca2161712d6e16f100cf6999e21fc64c3ee8e557d663c526a30dc9eef3069d2f229a699304d5f1029eb61176c3c51f4f825f5a45ac9e56
6
+ metadata.gz: b69c79b3ab7b34499bf4760d36ea54401b10e1bd0d68b37e4b39796b0a4fb48909a18fcbd724db619d95abf92f9585f094ec11c5d6c0332c3e4880c52654e977
7
+ data.tar.gz: ec9060115f34e789d1706e0165cd30fd5fb72c1451c6fa4b5df00b57fec18335ed81b960444a03424c5cf727fab6d32184357e0a442b93d86b2405b246f073e5
data/Readme.md CHANGED
@@ -65,11 +65,11 @@ end
65
65
  ```
66
66
 
67
67
  ### Adding a new monitor
68
- - use [datadog monitor UI](https://app.datadoghq.com/monitors#create/metric) to create a monitor
68
+ - use [datadog monitor UI](https://app.datadoghq.com/monitors#create) to create a monitor
69
69
  - see below
70
70
 
71
71
  ### Updating an existing monitor
72
- - use [datadog monitor UI](https://app.datadoghq.com/monitors#create/metric) to find a monitor
72
+ - use [datadog monitor UI](https://app.datadoghq.com/monitors/manage) to find a monitor
73
73
  - get the `id` from the url
74
74
  - run `RESOURCE=monitor ID=12345 bundle exec rake kennel:import` and copy the output
75
75
  - find or create a project in `projects/`
data/lib/kennel.rb CHANGED
@@ -10,16 +10,19 @@ require "kennel/syncer"
10
10
  require "kennel/api"
11
11
  require "kennel/github_reporter"
12
12
  require "kennel/subclass_tracking"
13
+ require "kennel/settings_as_methods"
13
14
  require "kennel/file_cache"
14
15
  require "kennel/template_variables"
15
16
  require "kennel/optional_validations"
16
17
  require "kennel/unmuted_alerts"
17
18
 
18
19
  require "kennel/models/base"
20
+ require "kennel/models/record"
19
21
 
20
- # parts
21
- require "kennel/models/monitor"
22
+ # records
22
23
  require "kennel/models/dashboard"
24
+ require "kennel/models/monitor"
25
+ require "kennel/models/slo"
23
26
 
24
27
  # settings
25
28
  require "kennel/models/project"
data/lib/kennel/api.rb CHANGED
@@ -8,7 +8,8 @@ module Kennel
8
8
  end
9
9
 
10
10
  def show(api_resource, id, params = {})
11
- request :get, "/api/v1/#{api_resource}/#{id}", params: params
11
+ reply = request :get, "/api/v1/#{api_resource}/#{id}", params: params
12
+ api_resource == "slo" ? reply[:data] : reply
12
13
  end
13
14
 
14
15
  def list(api_resource, params = {})
@@ -16,7 +17,8 @@ module Kennel
16
17
  end
17
18
 
18
19
  def create(api_resource, attributes)
19
- request :post, "/api/v1/#{api_resource}", body: attributes
20
+ reply = request :post, "/api/v1/#{api_resource}", body: attributes
21
+ api_resource == "slo" ? reply[:data].first : reply
20
22
  end
21
23
 
22
24
  def update(api_resource, id, attributes)
@@ -28,7 +28,7 @@ module Kennel
28
28
 
29
29
  title_field = TITLES.detect { |f| data[f] }
30
30
  title = data.fetch(title_field)
31
- title.tr!(Kennel::Models::Base::LOCK, "") # avoid double lock icon
31
+ title.tr!(Kennel::Models::Record::LOCK, "") # avoid double lock icon
32
32
 
33
33
  # calculate or reuse kennel_id
34
34
  # TODO: this is copy-pasted from syncer, need to find a nice way to reuse it
@@ -53,6 +53,8 @@ module Kennel
53
53
  if query && critical
54
54
  query.sub!(/([><=]) (#{Regexp.escape(critical.to_f.to_s)}|#{Regexp.escape(critical.to_i.to_s)})$/, "\\1 \#{critical}")
55
55
  end
56
+
57
+ data[:type] = "query alert" if data[:type] == "metric alert"
56
58
  elsif resource == "dashboard"
57
59
  widgets = data[:widgets]&.flat_map { |widget| widget.dig(:definition, :widgets) || [widget] }
58
60
  widgets&.each { |widget| dry_up_query!(widget) }
@@ -105,7 +107,9 @@ module Kennel
105
107
 
106
108
  "\n#{pretty}\n "
107
109
  elsif k == :message
108
- "\n <<~TEXT\n#{v.each_line.map { |l| l.strip.empty? ? "\n" : " #{l}" }.join}\n TEXT\n "
110
+ "\n <<~TEXT\n#{v.each_line.map { |l| l.strip.empty? ? "\n" : " #{l}" }.join}\n \#{super()}\n TEXT\n "
111
+ elsif k == :tags
112
+ " super() + #{v.inspect} "
109
113
  else
110
114
  " #{v.inspect} "
111
115
  end
@@ -4,115 +4,10 @@ require "hashdiff"
4
4
  module Kennel
5
5
  module Models
6
6
  class Base
7
- LOCK = "\u{1F512}"
8
- READONLY_ATTRIBUTES = [
9
- :deleted, :matching_downtimes, :id, :created, :created_at, :creator, :org_id, :modified,
10
- :overall_state_modified, :overall_state, :api_resource
11
- ].freeze
12
- REQUEST_DEFAULTS = {
13
- style: { width: "normal", palette: "dog_classic", type: "solid" },
14
- conditional_formats: [],
15
- aggregator: "avg"
16
- }.freeze
17
- OVERRIDABLE_METHODS = [:name, :kennel_id].freeze
7
+ extend SubclassTracking
8
+ include SettingsAsMethods
18
9
 
19
- class ValidationError < RuntimeError
20
- end
21
-
22
- class << self
23
- include SubclassTracking
24
-
25
- def settings(*names)
26
- duplicates = (@set & names)
27
- if duplicates.any?
28
- raise ArgumentError, "Settings #{duplicates.map(&:inspect).join(", ")} are already defined"
29
- end
30
-
31
- overrides = ((instance_methods - OVERRIDABLE_METHODS) & names)
32
- if overrides.any?
33
- raise ArgumentError, "Settings #{overrides.map(&:inspect).join(", ")} are already used as methods"
34
- end
35
-
36
- @set.concat names
37
- names.each do |name|
38
- next if method_defined?(name)
39
- define_method name do
40
- message = "Trying to call #{name} for #{self.class} but it was never set or passed as option"
41
- raise_with_location ArgumentError, message
42
- end
43
- end
44
- end
45
-
46
- def defaults(options)
47
- options.each do |name, block|
48
- validate_setting_exists name
49
- define_method name, &block
50
- end
51
- end
52
-
53
- def inherited(child)
54
- super
55
- child.instance_variable_set(:@set, (@set || []).dup)
56
- end
57
-
58
- def validate_setting_exists(name)
59
- return if !@set || @set.include?(name)
60
- supported = @set.map(&:inspect)
61
- raise ArgumentError, "Unsupported setting #{name.inspect}, supported settings are #{supported.join(", ")}"
62
- end
63
-
64
- private
65
-
66
- def normalize(_expected, actual)
67
- self::READONLY_ATTRIBUTES.each { |k| actual.delete k }
68
- end
69
-
70
- # discard styles/conditional_formats/aggregator if nothing would change when we applied (both are default or nil)
71
- def ignore_request_defaults(expected, actual, level1, level2)
72
- actual = actual[level1] || {}
73
- expected = expected[level1] || {}
74
- [expected.size.to_i, actual.size.to_i].max.times do |i|
75
- a_r = actual.dig(i, level2, :requests) || []
76
- e_r = expected.dig(i, level2, :requests) || []
77
- ignore_defaults e_r, a_r, self::REQUEST_DEFAULTS
78
- end
79
- end
80
-
81
- def ignore_defaults(expected, actual, defaults)
82
- [expected&.size.to_i, actual&.size.to_i].max.times do |i|
83
- e = expected[i] || {}
84
- a = actual[i] || {}
85
- ignore_default(e, a, defaults)
86
- end
87
- end
88
-
89
- def ignore_default(expected, actual, defaults)
90
- definitions = [actual, expected]
91
- defaults.each do |key, default|
92
- if definitions.all? { |r| !r.key?(key) || r[key] == default }
93
- actual.delete(key)
94
- expected.delete(key)
95
- end
96
- end
97
- end
98
- end
99
-
100
- def initialize(options = {})
101
- validate_options(options)
102
-
103
- options.each do |name, block|
104
- self.class.validate_setting_exists name
105
- define_singleton_method name, &block
106
- end
107
-
108
- # need expand_path so it works wih rake and when run individually
109
- pwd = /^#{Regexp.escape(Dir.pwd)}\//
110
- @invocation_location = caller.detect do |l|
111
- if found = File.expand_path(l).sub!(pwd, "")
112
- break found
113
- end
114
- end
115
- end
10
+ SETTING_OVERRIDABLE_METHODS = [:name, :kennel_id].freeze
116
11
 
117
12
  def kennel_id
118
13
  name = self.class.name
@@ -126,56 +21,9 @@ module Kennel
126
21
  self.class.name
127
22
  end
128
23
 
129
- def diff(actual)
130
- expected = as_json
131
- expected.delete(:id)
132
-
133
- self.class.send(:normalize, expected, actual)
134
-
135
- HashDiff.diff(actual, expected, use_lcs: false)
136
- end
137
-
138
- def tracking_id
139
- "#{project.kennel_id}:#{kennel_id}"
140
- end
141
-
142
24
  def to_json
143
25
  raise NotImplementedError, "Use as_json"
144
26
  end
145
-
146
- def resolve_linked_tracking_ids(*)
147
- end
148
-
149
- private
150
-
151
- def resolve_link(id, id_map, force:)
152
- id_map[id] || begin
153
- message = "Unable to find #{id} in existing monitors (they need to be created first to link them)"
154
- force ? invalid!(message) : Kennel.err.puts(message)
155
- end
156
- end
157
-
158
- # let users know which project/resource failed when something happens during diffing where the backtrace is hidden
159
- def invalid!(message)
160
- raise ValidationError, "#{tracking_id} #{message}"
161
- end
162
-
163
- def raise_with_location(error, message)
164
- message = message.dup
165
- message << " for project #{project.kennel_id}" if defined?(project)
166
- message << " on #{@invocation_location}" if @invocation_location
167
- raise error, message
168
- end
169
-
170
- def validate_options(options)
171
- unless options.is_a?(Hash)
172
- raise ArgumentError, "Expected #{self.class.name}.new options to be a Hash, got a #{options.class}"
173
- end
174
- options.each do |k, v|
175
- next if v.class == Proc
176
- raise ArgumentError, "Expected #{self.class.name}.new option :#{k} to be Proc, for example `#{k}: -> { 12 }`"
177
- end
178
- end
179
27
  end
180
28
  end
181
29
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
3
  module Models
4
- class Dashboard < Base
4
+ class Dashboard < Record
5
5
  include TemplateVariables
6
6
  include OptionalValidations
7
7
 
8
8
  API_LIST_INCOMPLETE = true
9
9
  DASHBOARD_DEFAULTS = { template_variables: [] }.freeze
10
- READONLY_ATTRIBUTES = Base::READONLY_ATTRIBUTES + [
10
+ READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
11
11
  :author_handle, :author_name, :modified_at, :url, :is_read_only, :notify_list
12
12
  ]
13
13
  REQUEST_DEFAULTS = {
@@ -15,7 +15,7 @@ module Kennel
15
15
  }.freeze
16
16
  SUPPORTED_DEFINITION_OPTIONS = [:events, :markers, :precision].freeze
17
17
 
18
- settings :id, :title, :description, :definitions, :widgets, :kennel_id, :layout_type
18
+ settings :title, :description, :definitions, :widgets, :layout_type
19
19
 
20
20
  defaults(
21
21
  description: -> { "" },
@@ -32,9 +32,15 @@ module Kennel
32
32
  def normalize(expected, actual)
33
33
  super
34
34
 
35
- ignore_default expected, actual, DASHBOARD_DEFAULTS
36
-
37
35
  base_pairs(expected, actual).each do |pair|
36
+ # datadog always adds 2 to slo widget height
37
+ # need to check fir layout since some monitors have height/width in their definition
38
+ pair.dig(1, :widgets)&.each do |widget|
39
+ if widget.dig(:definition, :type) == "slo" && widget.dig(:layout, :height)
40
+ widget[:layout][:height] -= 2
41
+ end
42
+ end
43
+
38
44
  # conditional_formats ordering is randomly changed by datadog, compare a stable ordering
39
45
  pair.each do |b|
40
46
  b[:widgets]&.each do |w|
@@ -64,13 +70,6 @@ module Kennel
64
70
  end
65
71
  end
66
72
 
67
- attr_reader :project
68
-
69
- def initialize(project, *args)
70
- @project = project
71
- super(*args)
72
- end
73
-
74
73
  def as_json
75
74
  return @json if @json
76
75
  all_widgets = render_definitions + widgets
@@ -109,6 +108,10 @@ module Kennel
109
108
  if (id = definition[:alert_id]) && tracking_id?(id)
110
109
  definition[:alert_id] = resolve_link(id, id_map, force: false).to_s
111
110
  end
111
+ when "slo"
112
+ if (id = definition[:slo_id]) && tracking_id?(id)
113
+ definition[:slo_id] = resolve_link(id, id_map, force: false).to_s
114
+ end
112
115
  end
113
116
  end
114
117
  end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
3
  module Models
4
- class Monitor < Base
4
+ class Monitor < Record
5
5
  include OptionalValidations
6
6
 
7
- API_LIST_INCOMPLETE = false
8
7
  RENOTIFY_INTERVALS = [0, 10, 20, 30, 40, 50, 60, 90, 120, 180, 240, 300, 360, 720, 1440].freeze # minutes
9
8
  QUERY_INTERVALS = ["1m", "5m", "10m", "15m", "30m", "1h", "2h", "4h", "1d"].freeze
10
9
  OPTIONAL_SERVICE_CHECK_THRESHOLDS = [:ok, :warning].freeze
11
- READONLY_ATTRIBUTES = Base::READONLY_ATTRIBUTES + [:multi]
10
+ READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
11
+ :multi, :matching_downtimes, :overall_state_modified, :overall_state
12
+ ]
12
13
 
13
14
  # defaults that datadog uses when options are not sent, so safe to leave out if our values match their defaults
14
15
  MONITOR_OPTION_DEFAULTS = {
@@ -21,8 +22,8 @@ module Kennel
21
22
  DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
22
23
 
23
24
  settings(
24
- :query, :name, :message, :escalation_message, :critical, :kennel_id, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
25
- :ok, :id, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
25
+ :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
26
+ :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
26
27
  :threshold_windows, :new_host_delay
27
28
  )
28
29
 
@@ -45,13 +46,6 @@ module Kennel
45
46
  threshold_windows: -> { nil }
46
47
  )
47
48
 
48
- attr_reader :project
49
-
50
- def initialize(project, *args)
51
- @project = project
52
- super(*args)
53
- end
54
-
55
49
  def as_json
56
50
  return @as_json if @as_json
57
51
  data = {
@@ -108,8 +102,6 @@ module Kennel
108
102
  @as_json = data
109
103
  end
110
104
 
111
- # resolve composite monitors ... only works when referenced monitors already exist
112
- # since leaving names or bad ids in the query breaks the monitor update
113
105
  def resolve_linked_tracking_ids(id_map)
114
106
  if as_json[:type] == "composite"
115
107
  as_json[:query] = as_json[:query].gsub(/%\{(.*?)\}/) do
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ module Models
4
+ class Record < Base
5
+ LOCK = "\u{1F512}"
6
+ READONLY_ATTRIBUTES = [
7
+ :deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at, :api_resource
8
+ ].freeze
9
+ REQUEST_DEFAULTS = {
10
+ style: { width: "normal", palette: "dog_classic", type: "solid" },
11
+ conditional_formats: [],
12
+ aggregator: "avg"
13
+ }.freeze
14
+ API_LIST_INCOMPLETE = false
15
+
16
+ class ValidationError < RuntimeError
17
+ end
18
+
19
+ settings :id, :kennel_id
20
+
21
+ class << self
22
+ private
23
+
24
+ def normalize(_expected, actual)
25
+ self::READONLY_ATTRIBUTES.each { |k| actual.delete k }
26
+ end
27
+
28
+ # discard styles/conditional_formats/aggregator if nothing would change when we applied (both are default or nil)
29
+ def ignore_request_defaults(expected, actual, level1, level2)
30
+ actual = actual[level1] || {}
31
+ expected = expected[level1] || {}
32
+ [expected.size.to_i, actual.size.to_i].max.times do |i|
33
+ a_r = actual.dig(i, level2, :requests) || []
34
+ e_r = expected.dig(i, level2, :requests) || []
35
+ ignore_defaults e_r, a_r, self::REQUEST_DEFAULTS
36
+ end
37
+ end
38
+
39
+ def ignore_defaults(expected, actual, defaults)
40
+ [expected&.size.to_i, actual&.size.to_i].max.times do |i|
41
+ e = expected[i] || {}
42
+ a = actual[i] || {}
43
+ ignore_default(e, a, defaults)
44
+ end
45
+ end
46
+
47
+ def ignore_default(expected, actual, defaults)
48
+ definitions = [actual, expected]
49
+ defaults.each do |key, default|
50
+ if definitions.all? { |r| !r.key?(key) || r[key] == default }
51
+ actual.delete(key)
52
+ expected.delete(key)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ attr_reader :project
59
+
60
+ def initialize(project, *args)
61
+ raise ArgumentError, "First argument must be a project, not #{project.class}" unless project.is_a?(Project)
62
+ @project = project
63
+ super(*args)
64
+ end
65
+
66
+ def diff(actual)
67
+ expected = as_json
68
+ expected.delete(:id)
69
+
70
+ self.class.send(:normalize, expected, actual)
71
+
72
+ HashDiff.diff(actual, expected, use_lcs: false)
73
+ end
74
+
75
+ def tracking_id
76
+ "#{project.kennel_id}:#{kennel_id}"
77
+ end
78
+
79
+ def resolve_linked_tracking_ids(*)
80
+ end
81
+
82
+ private
83
+
84
+ def resolve_link(id, id_map, force:)
85
+ id_map[id] || begin
86
+ message = "Unable to find #{id} in existing monitors (they need to be created first to link them)"
87
+ force ? invalid!(message) : Kennel.err.puts(message)
88
+ end
89
+ end
90
+
91
+ # let users know which project/resource failed when something happens during diffing where the backtrace is hidden
92
+ def invalid!(message)
93
+ raise ValidationError, "#{tracking_id} #{message}"
94
+ end
95
+
96
+ def raise_with_location(error, message)
97
+ super error, "#{message} for project #{project.kennel_id}"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ module Models
4
+ class Slo < Record
5
+ READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:type_id, :monitor_tags]
6
+ DEFAULTS = {
7
+ description: nil,
8
+ query: nil,
9
+ monitor_ids: []
10
+ }.freeze
11
+
12
+ settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name
13
+
14
+ defaults(
15
+ id: -> { nil },
16
+ tags: -> { @project.tags },
17
+ query: -> { DEFAULTS.fetch(:query) },
18
+ description: -> { DEFAULTS.fetch(:description) },
19
+ monitor_ids: -> { DEFAULTS.fetch(:monitor_ids) }
20
+ )
21
+
22
+ def as_json
23
+ return @as_json if @as_json
24
+ data = {
25
+ name: "#{name}#{LOCK}",
26
+ description: description,
27
+ thresholds: thresholds,
28
+ monitor_ids: monitor_ids,
29
+ tags: tags,
30
+ type: type
31
+ }
32
+
33
+ data[:query] = query if query
34
+ data[:id] = id if id
35
+
36
+ @as_json = data
37
+ end
38
+
39
+ def self.api_resource
40
+ "slo"
41
+ end
42
+
43
+ def url(id)
44
+ Utils.path_to_url "/slo?slo_id=#{id}"
45
+ end
46
+
47
+ def resolve_linked_tracking_ids(id_map)
48
+ as_json[:monitor_ids] = as_json[:monitor_ids].map do |id|
49
+ id.is_a?(String) ? resolve_link(id, id_map, force: false) || 1 : id
50
+ end
51
+ end
52
+
53
+ def self.normalize(expected, actual)
54
+ super
55
+
56
+ # remove readonly values
57
+ actual[:thresholds]&.each do |threshold|
58
+ threshold.delete(:warning_display)
59
+ threshold.delete(:target_display)
60
+ end
61
+
62
+ # tags come in a semi-random order and order is never updated
63
+ expected[:tags]&.sort!
64
+ actual[:tags].sort!
65
+
66
+ ignore_default(expected, actual, DEFAULTS)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -6,19 +6,10 @@ module Kennel
6
6
  base.defaults(validate: -> { true })
7
7
  end
8
8
 
9
- # https://stackoverflow.com/questions/20235206/ruby-get-all-keys-in-a-hash-including-sub-keys/53876255#53876255
10
- def self.all_keys(items)
11
- case items
12
- when Hash then items.keys + items.values.flat_map { |v| all_keys(v) }
13
- when Array then items.flat_map { |i| all_keys(i) }
14
- else []
15
- end
16
- end
17
-
18
9
  private
19
10
 
20
11
  def validate_json(data)
21
- bad = OptionalValidations.all_keys(data).grep_v(Symbol)
12
+ bad = Kennel::Utils.all_keys(data).grep_v(Symbol)
22
13
  return if bad.empty?
23
14
  invalid!(
24
15
  "Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ module SettingsAsMethods
4
+ SETTING_OVERRIDABLE_METHODS = [].freeze
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.instance_variable_set(:@settings, [])
9
+ end
10
+
11
+ module ClassMethods
12
+ def settings(*names)
13
+ duplicates = (@settings & names)
14
+ if duplicates.any?
15
+ raise ArgumentError, "Settings #{duplicates.map(&:inspect).join(", ")} are already defined"
16
+ end
17
+
18
+ overrides = ((instance_methods - self::SETTING_OVERRIDABLE_METHODS) & names)
19
+ if overrides.any?
20
+ raise ArgumentError, "Settings #{overrides.map(&:inspect).join(", ")} are already used as methods"
21
+ end
22
+
23
+ @settings.concat names
24
+
25
+ names.each do |name|
26
+ next if method_defined?(name)
27
+ define_method name do
28
+ message = "Trying to call #{name} for #{self.class} but it was never set or passed as option"
29
+ raise_with_location ArgumentError, message
30
+ end
31
+ end
32
+ end
33
+
34
+ def defaults(options)
35
+ options.each do |name, block|
36
+ validate_setting_exist name
37
+ define_method name, &block
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def validate_setting_exist(name)
44
+ return if !@settings || @settings.include?(name)
45
+ supported = @settings.map(&:inspect)
46
+ raise ArgumentError, "Unsupported setting #{name.inspect}, supported settings are #{supported.join(", ")}"
47
+ end
48
+
49
+ def inherited(child)
50
+ super
51
+ child.instance_variable_set(:@settings, (@settings || []).dup)
52
+ end
53
+ end
54
+
55
+ def initialize(options = {})
56
+ super()
57
+
58
+ unless options.is_a?(Hash)
59
+ raise ArgumentError, "Expected #{self.class.name}.new options to be a Hash, got a #{options.class}"
60
+ end
61
+
62
+ options.each do |k, v|
63
+ next if v.class == Proc
64
+ raise ArgumentError, "Expected #{self.class.name}.new option :#{k} to be Proc, for example `#{k}: -> { 12 }`"
65
+ end
66
+
67
+ options.each do |name, block|
68
+ self.class.send :validate_setting_exist, name
69
+ define_singleton_method name, &block
70
+ end
71
+
72
+ # need expand_path so it works wih rake and when run individually
73
+ pwd = /^#{Regexp.escape(Dir.pwd)}\//
74
+ @invocation_location = caller.detect do |l|
75
+ if found = File.expand_path(l).sub!(pwd, "")
76
+ break found
77
+ end
78
+ end
79
+ end
80
+
81
+ def raise_with_location(error, message)
82
+ message = message.dup
83
+ message << " on #{@invocation_location}" if @invocation_location
84
+ raise error, message
85
+ end
86
+ end
87
+ end
data/lib/kennel/syncer.rb CHANGED
@@ -71,28 +71,28 @@ module Kennel
71
71
  Progress.progress "Diffing" do
72
72
  filter_by_project! actual
73
73
 
74
- details_cache do |cache|
75
- items = actual.map do |a|
76
- e = matching_expected(a)
77
- if e && @expected.delete(e)
78
- [e, a]
79
- else
80
- [nil, a]
81
- end
74
+ items = actual.map do |a|
75
+ e = matching_expected(a)
76
+ if e && @expected.delete(e)
77
+ [e, a]
78
+ else
79
+ [nil, a]
82
80
  end
81
+ end
83
82
 
83
+ details_cache do |cache|
84
84
  # fill details of things we need to compare (only do this part in parallel for safety & balancing)
85
85
  Utils.parallel(items.select { |e, _| e && e.class::API_LIST_INCOMPLETE }) { |_, a| fill_details(a, cache) }
86
+ end
86
87
 
87
- # pick out things to update or delete
88
- items.each do |e, a|
89
- id = a.fetch(:id)
90
- if e
91
- diff = e.diff(a)
92
- @update << [id, e, a, diff] if diff.any?
93
- elsif tracking_id(a) # was previously managed
94
- @delete << [id, nil, a]
95
- end
88
+ # pick out things to update or delete
89
+ items.each do |e, a|
90
+ id = a.fetch(:id)
91
+ if e
92
+ diff = e.diff(a)
93
+ @update << [id, e, a, diff] if diff.any?
94
+ elsif tracking_id(a) # was previously managed
95
+ @delete << [id, nil, a]
96
96
  end
97
97
  end
98
98
 
@@ -113,7 +113,7 @@ module Kennel
113
113
 
114
114
  # dashes are nested, others are not
115
115
  def unnest(api_resource, result)
116
- result[api_resource.to_sym] || result
116
+ result[api_resource.to_sym] || result[:data] || result
117
117
  end
118
118
 
119
119
  def details_cache(&block)
@@ -122,14 +122,9 @@ module Kennel
122
122
  end
123
123
 
124
124
  def download_definitions
125
- api_resources = Models::Base.subclasses.map do |m|
126
- next unless m.respond_to?(:api_resource)
127
- m.api_resource
128
- end
129
-
130
- Utils.parallel(api_resources.compact.uniq) do |api_resource|
125
+ Utils.parallel(Models::Record.subclasses.map(&:api_resource)) do |api_resource|
131
126
  results = @api.list(api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
132
- results = results[results.keys.first] if results.is_a?(Hash) # dashes/screens are nested in {dash: {}}
127
+ results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
133
128
  results.each { |c| c[:api_resource] = api_resource } # store api resource for later diffing
134
129
  end.flatten(1)
135
130
  end
data/lib/kennel/tasks.rb CHANGED
@@ -38,6 +38,7 @@ namespace :kennel do
38
38
  url = (subdomain ? "https://zendesk.datadoghq.com" : "") + "/account/settings"
39
39
  puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
40
40
  bad.each { |f, v| puts "Invalid mention #{v} in monitor message of #{f}" }
41
+ abort
41
42
  end
42
43
  end
43
44
 
data/lib/kennel/utils.rb CHANGED
@@ -133,6 +133,15 @@ module Kennel
133
133
  Kennel.err.puts "Error #{e}, #{times} retries left"
134
134
  retry
135
135
  end
136
+
137
+ # https://stackoverflow.com/questions/20235206/ruby-get-all-keys-in-a-hash-including-sub-keys/53876255#53876255
138
+ def all_keys(items)
139
+ case items
140
+ when Hash then items.keys + items.values.flat_map { |v| all_keys(v) }
141
+ when Array then items.flat_map { |i| all_keys(i) }
142
+ else []
143
+ end
144
+ end
136
145
  end
137
146
  end
138
147
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.55.0"
3
+ VERSION = "1.57.2"
4
4
  end
data/template/Readme.md CHANGED
@@ -47,11 +47,11 @@ end
47
47
  ```
48
48
 
49
49
  ### Adding a new monitor
50
- - use [datadog monitor UI](https://app.datadoghq.com/monitors#create/metric) to create a monitor
50
+ - use [datadog monitor UI](https://app.datadoghq.com/monitors#create) to create a monitor
51
51
  - see below
52
52
 
53
53
  ### Updating an existing monitor
54
- - use [datadog monitor UI](https://app.datadoghq.com/monitors#create/metric) to find a monitor
54
+ - use [datadog monitor UI](https://app.datadoghq.com/monitors/manage) to find a monitor
55
55
  - get the `id` from the url
56
56
  - run `RESOURCE=monitor ID=12345 bundle exec rake kennel:import` and copy the output
57
57
  - find or create a project in `projects/`
@@ -161,6 +161,10 @@ To link to existing monitors via their kennel_id
161
161
 
162
162
  Run `rake kennel:alerts TAG=service:my-service` to see all un-muted alerts for a given datadog monitor tag.
163
163
 
164
+ ### Validating mentions work
165
+
166
+ `rake kennel:validate_mentions` should run as part of CI
167
+
164
168
  ## Examples
165
169
 
166
170
  ### Reusable monitors/dashes/etc
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.55.0
4
+ version: 1.57.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-14 00:00:00.000000000 Z
11
+ date: 2019-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -68,9 +68,12 @@ files:
68
68
  - lib/kennel/models/dashboard.rb
69
69
  - lib/kennel/models/monitor.rb
70
70
  - lib/kennel/models/project.rb
71
+ - lib/kennel/models/record.rb
72
+ - lib/kennel/models/slo.rb
71
73
  - lib/kennel/models/team.rb
72
74
  - lib/kennel/optional_validations.rb
73
75
  - lib/kennel/progress.rb
76
+ - lib/kennel/settings_as_methods.rb
74
77
  - lib/kennel/subclass_tracking.rb
75
78
  - lib/kennel/syncer.rb
76
79
  - lib/kennel/tasks.rb