kennel 1.55.1 → 1.58.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e23afaba22ded8b1fb654a9552ed135247cb3a9a5c5610090bfa9096123ffb4
4
- data.tar.gz: 7e4096a43a6fcd52f519131c2238879f2eb551d90c5883a9d5e1e8996f90075c
3
+ metadata.gz: ffd28c1dcb7db623d9e93d2238e143e78c928f76573d97781c1bdcd058c0ab1c
4
+ data.tar.gz: 6948f95652b107a8ccf3f58585cd2e3c4e908554a8c99ecade86ad6233eb8d14
5
5
  SHA512:
6
- metadata.gz: bb7d4a1e6f48f645e220169bcabac5ae8fed56f01033a63be6ea855e5308e62ec3d31a4c9326823d99e973d85056f1151a1924ae33ced07b620224b1e716eb8b
7
- data.tar.gz: 7bb59bc7ca8bd3b024903c61bdc11c4f816d9fca0fc666cae36dcdb6258d660608ba6cfe16ccbf1877957c54eb9e9feda437055d44fe4f5a3e0abe701623680d
6
+ metadata.gz: 3c5bbfb1b9bc9a9c32e9200b3572f01bb201aade69081dc651dbb499d35816d157593a41aa057669f0dc2fd6f9e2cd4d99235b88a7c7e09b70f032a5c2e0ae80
7
+ data.tar.gz: fdbfa3f3f8b9c654e60e072909bcb1f6d30580142a03a5365220f781b062e821f6d8313538b2aefc87ed64974f239df0f0c9b4a7798ca5ad4560d5aa3656c907
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,79 @@
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
+ thresholds: []
11
+ }.freeze
12
+
13
+ settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name
14
+
15
+ defaults(
16
+ id: -> { nil },
17
+ tags: -> { @project.tags },
18
+ query: -> { DEFAULTS.fetch(:query) },
19
+ description: -> { DEFAULTS.fetch(:description) },
20
+ monitor_ids: -> { DEFAULTS.fetch(:monitor_ids) },
21
+ thresholds: -> { DEFAULTS.fetch(:thresholds) }
22
+ )
23
+
24
+ def initialize(*)
25
+ super
26
+ if thresholds.any? { |t| t[:warning] && t[:warning].to_f <= t[:critical].to_f }
27
+ raise ValidationError, "Threshold warning must be greater-than critical value"
28
+ end
29
+ end
30
+
31
+ def as_json
32
+ return @as_json if @as_json
33
+ data = {
34
+ name: "#{name}#{LOCK}",
35
+ description: description,
36
+ thresholds: thresholds,
37
+ monitor_ids: monitor_ids,
38
+ tags: tags,
39
+ type: type
40
+ }
41
+
42
+ data[:query] = query if query
43
+ data[:id] = id if id
44
+
45
+ @as_json = data
46
+ end
47
+
48
+ def self.api_resource
49
+ "slo"
50
+ end
51
+
52
+ def url(id)
53
+ Utils.path_to_url "/slo?slo_id=#{id}"
54
+ end
55
+
56
+ def resolve_linked_tracking_ids(id_map)
57
+ as_json[:monitor_ids] = as_json[:monitor_ids].map do |id|
58
+ id.is_a?(String) ? resolve_link(id, id_map, force: false) || 1 : id
59
+ end
60
+ end
61
+
62
+ def self.normalize(expected, actual)
63
+ super
64
+
65
+ # remove readonly values
66
+ actual[:thresholds]&.each do |threshold|
67
+ threshold.delete(:warning_display)
68
+ threshold.delete(:target_display)
69
+ end
70
+
71
+ # tags come in a semi-random order and order is never updated
72
+ expected[:tags]&.sort!
73
+ actual[:tags].sort!
74
+
75
+ ignore_default(expected, actual, DEFAULTS)
76
+ end
77
+ end
78
+ end
79
+ 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/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.1"
3
+ VERSION = "1.58.0"
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.1
4
+ version: 1.58.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-17 00:00:00.000000000 Z
11
+ date: 2019-11-21 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