kennel 1.78.4 → 1.81.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: 23d5816783075cabd0b6a51addfe89bd0d21f4935a547fd1bfbd0c754d6f3b18
4
- data.tar.gz: 73df009d112f1af7d1500428e700ac6cdea3d99cbb36a12dd3ceef926b2f5fd4
3
+ metadata.gz: 5f9d3050dbb7a4e94cd22e64618142bedffd6fdfc5b5ee8e20b621b90f1d8f27
4
+ data.tar.gz: 640a86b74e46e27ec7f1b09dd555f0ba8f99e4fc1f687e5230bb00d12636ae1d
5
5
  SHA512:
6
- metadata.gz: 04a237a134852bd15c4ce7eb6a309c0f8526c656f646269ccfeaf8f148013ea6c8328bcfbaffaee53c228af1e6b79c021ff9cade7afacb8a301f4e418302dbd0
7
- data.tar.gz: 3e51d0361af539cd3aca894f51d8e28cf5ef613722c1f3a7f7b5f3f88fb74e43ab535034144d246cc0f079d5d06e9e62f488bb76257390cae4a7f0f14636984e
6
+ metadata.gz: 80dbfff63550b6f5c3127cb8ecc97db6b3db38876469bcb9d50fce00a7e1b78e2f473dd3c3abaec081cb21037f53be72021d907c4285352b30dd3b6991f6c622
7
+ data.tar.gz: 02a3122ec70a951dc01f919c4d13877de88c6266048d006770a62adadd19587d74a603e8281699f878be0a20227a7086e37727c751cf5833e815fc795e3f9ee3
data/Readme.md CHANGED
@@ -84,7 +84,7 @@ end
84
84
  - `cp .env.example .env`
85
85
  - open [Datadog API Settings](https://app.datadoghq.com/account/settings#api)
86
86
  - create a `API Key` or get an existing one from an admin, then add it to `.env` as `DATADOG_API_KEY`
87
- - find or create (check last page) your personal "Application Key" and add it to `.env` as `DATADOG_APP_KEY=`
87
+ - open [Datadog API Settings](https://app.datadoghq.com/access/application-keys) and create a new key, then add it to `.env` as `DATADOG_APP_KEY=`
88
88
  - change the `DATADOG_SUBDOMAIN=app` in `.env` to your companies subdomain if you have one
89
89
  - verify it works by running `rake plan`, it might show some diff, but should not crash
90
90
  -->
@@ -212,14 +212,20 @@ removing the `id` will cause kennel to create a new resource in datadog.
212
212
  Some validations might be too strict for your usecase or just wrong, please [open an issue](https://github.com/grosser/kennel/issues) and
213
213
  to unblock use the `validate: -> { false }` option.
214
214
 
215
- ### Linking with kennel_ids
215
+ ### Linking resources with kennel_id
216
216
 
217
- To link to existing monitors via their kennel_id `projects kennel_id` + `:` + `monitors kennel id`
217
+ Link resources with their kennel_id in the format `project kennel_id` + `:` + `resource kennel_id`,
218
+ this should be used to create dependent resources like monitor + slos,
219
+ so they can be created in a single update and can be re-created if any of them is deleted.
218
220
 
219
- - Screens `uptime` widgets can use `monitor: {id: "foo:bar"}`
220
- - Screens `alert_graph` widgets can use `alert_id: "foo:bar"`
221
- - Monitors `composite` can use `query: -> { "%{foo:bar} || %{foo:baz}" }`
222
- - Slos can use `monitor_ids: -> ["foo:bar"]`
221
+ |Resource|Type|Syntax|
222
+ |---|---|---|
223
+ |Dashboard|uptime|`monitor: {id: "foo:bar"}`|
224
+ |Dashboard|alert_graph|`alert_id: "foo:bar"`|
225
+ |Dashboard|slo|`slo_id: "foo:bar"`|
226
+ |Monitor|composite|`query: -> { "%{foo:bar} && %{foo:baz}" }`|
227
+ |Monitor|slo alert|`query: -> { "error_budget(\"%{foo:bar}\").over(\"7d\") > 123.0" }`|
228
+ |Slo|monitor|`monitor_ids: -> ["foo:bar"]`|
223
229
 
224
230
  ### Debugging changes locally
225
231
 
@@ -296,5 +302,5 @@ Author
296
302
  [Michael Grosser](http://grosser.it)<br/>
297
303
  michael@grosser.it<br/>
298
304
  License: MIT<br/>
299
- [![Build Status](https://travis-ci.org/grosser/kennel.png)](https://travis-ci.org/grosser/kennel)
305
+ ![CI](https://github.com/grosser/kennel/workflows/CI/badge.svg)
300
306
  <!-- NOT IN -->
data/lib/kennel.rb CHANGED
@@ -39,12 +39,7 @@ module Kennel
39
39
  attr_accessor :out, :err
40
40
 
41
41
  def generate
42
- FileUtils.rm_rf("generated")
43
- generated.each do |part|
44
- path = "generated/#{part.tracking_id.sub(":", "/")}.json"
45
- FileUtils.mkdir_p(File.dirname(path))
46
- File.write(path, JSON.pretty_generate(part.as_json) << "\n")
47
- end
42
+ store generated
48
43
  end
49
44
 
50
45
  def plan
@@ -58,6 +53,35 @@ module Kennel
58
53
 
59
54
  private
60
55
 
56
+ def store(parts)
57
+ Progress.progress "Storing" do
58
+ old = Dir["generated/**/*"]
59
+ used = []
60
+
61
+ Utils.parallel(parts, max: 2) do |part|
62
+ path = "generated/#{part.tracking_id.tr("/", ":").sub(":", "/")}.json"
63
+ used << File.dirname(path) # only 1 level of sub folders, so this is safe
64
+ used << path
65
+ write_file_if_necessary(path, JSON.pretty_generate(part.as_json) << "\n")
66
+ end
67
+
68
+ # deleting all is slow, so only delete the extras
69
+ (old - used).each { |p| FileUtils.rm_rf(p) }
70
+ end
71
+ end
72
+
73
+ def write_file_if_necessary(path, content)
74
+ # 99% case
75
+ begin
76
+ return if File.read(path) == content
77
+ rescue Errno::ENOENT
78
+ FileUtils.mkdir_p(File.dirname(path))
79
+ end
80
+
81
+ # slow 1% case
82
+ File.write(path, content)
83
+ end
84
+
61
85
  def syncer
62
86
  @syncer ||= Syncer.new(api, generated, project: ENV["PROJECT"])
63
87
  end
@@ -73,8 +97,12 @@ module Kennel
73
97
  parts = Models::Project.recursive_subclasses.flat_map do |project_class|
74
98
  project_class.new.validated_parts
75
99
  end
76
- parts.map(&:tracking_id).group_by { |id| id }.select do |id, same|
77
- raise "#{id} is defined #{same.size} times" if same.size != 1
100
+ parts.group_by(&:tracking_id).each do |tracking_id, same|
101
+ next if same.size == 1
102
+ raise <<~ERROR
103
+ #{tracking_id} is defined #{same.size} times
104
+ use a different `kennel_id` when defining multiple projects/monitors/dashboards to avoid this conflict
105
+ ERROR
78
106
  end
79
107
  parts
80
108
  end
@@ -140,16 +140,16 @@ module Kennel
140
140
  when "uptime"
141
141
  if ids = definition[:monitor_ids]
142
142
  definition[:monitor_ids] = ids.map do |id|
143
- tracking_id?(id) ? resolve_link(id, :monitor, id_map, **args) : id
143
+ tracking_id?(id) ? (resolve_link(id, :monitor, id_map, **args) || id) : id
144
144
  end
145
145
  end
146
146
  when "alert_graph"
147
147
  if (id = definition[:alert_id]) && tracking_id?(id)
148
- definition[:alert_id] = resolve_link(id, :monitor, id_map, **args).to_s
148
+ definition[:alert_id] = (resolve_link(id, :monitor, id_map, **args) || id).to_s
149
149
  end
150
150
  when "slo"
151
151
  if (id = definition[:slo_id]) && tracking_id?(id)
152
- definition[:slo_id] = resolve_link(id, :slo, id_map, **args).to_s
152
+ definition[:slo_id] = (resolve_link(id, :slo, id_map, **args) || id).to_s
153
153
  end
154
154
  end
155
155
  end
@@ -186,22 +186,26 @@ module Kennel
186
186
  end
187
187
 
188
188
  def render_definitions(definitions)
189
- definitions.map do |title, type, display_type, queries, options = {}, ignored = nil|
190
- # validate inputs
191
- if ignored || (!title || !type || !queries || !options.is_a?(Hash))
192
- raise ArgumentError, "Expected exactly 5 arguments for each definition (title, type, display_type, queries, options)"
193
- end
194
- if (SUPPORTED_DEFINITION_OPTIONS | options.keys) != SUPPORTED_DEFINITION_OPTIONS
195
- raise ArgumentError, "Supported options are: #{SUPPORTED_DEFINITION_OPTIONS.map(&:inspect).join(", ")}"
196
- end
189
+ definitions.map do |title, type, display_type, queries, options = {}, too_many_args = nil|
190
+ if title.is_a?(Hash) && !type
191
+ title # user gave a full widget, just use it
192
+ else
193
+ # validate inputs
194
+ if too_many_args || (!title || !type || !queries || !options.is_a?(Hash))
195
+ raise ArgumentError, "Expected exactly 5 arguments for each definition (title, type, display_type, queries, options)"
196
+ end
197
+ if (SUPPORTED_DEFINITION_OPTIONS | options.keys) != SUPPORTED_DEFINITION_OPTIONS
198
+ raise ArgumentError, "Supported options are: #{SUPPORTED_DEFINITION_OPTIONS.map(&:inspect).join(", ")}"
199
+ end
197
200
 
198
- # build definition
199
- requests = Array(queries).map do |q|
200
- request = { q: q }
201
- request[:display_type] = display_type if display_type
202
- request
201
+ # build definition
202
+ requests = Array(queries).map do |q|
203
+ request = { q: q }
204
+ request[:display_type] = display_type if display_type
205
+ request
206
+ end
207
+ { definition: { title: title, type: type, requests: requests, **options } }
203
208
  end
204
- { definition: { title: title, type: type, requests: requests, **options } }
205
209
  end
206
210
  end
207
211
  end
@@ -12,6 +12,10 @@ module Kennel
12
12
  :multi, :matching_downtimes, :overall_state_modified, :overall_state, :restricted_roles
13
13
  ]
14
14
 
15
+ MONITOR_DEFAULTS = {
16
+ priority: nil
17
+ }.freeze
18
+
15
19
  # defaults that datadog uses when options are not sent, so safe to leave out if our values match their defaults
16
20
  MONITOR_OPTION_DEFAULTS = {
17
21
  evaluation_delay: nil,
@@ -27,7 +31,7 @@ module Kennel
27
31
  settings(
28
32
  :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
29
33
  :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
30
- :threshold_windows, :new_host_delay
34
+ :threshold_windows, :new_host_delay, :priority
31
35
  )
32
36
 
33
37
  defaults(
@@ -46,7 +50,8 @@ module Kennel
46
50
  evaluation_delay: -> { MONITOR_OPTION_DEFAULTS.fetch(:evaluation_delay) },
47
51
  critical_recovery: -> { nil },
48
52
  warning_recovery: -> { nil },
49
- threshold_windows: -> { nil }
53
+ threshold_windows: -> { nil },
54
+ priority: -> { MONITOR_DEFAULTS.fetch(:priority) }
50
55
  )
51
56
 
52
57
  def as_json
@@ -57,6 +62,7 @@ module Kennel
57
62
  query: query.strip,
58
63
  message: message.strip,
59
64
  tags: tags.uniq,
65
+ priority: priority,
60
66
  options: {
61
67
  timeout_h: timeout_h,
62
68
  notify_no_data: notify_no_data,
@@ -106,9 +112,11 @@ module Kennel
106
112
  end
107
113
 
108
114
  def resolve_linked_tracking_ids!(id_map, **args)
109
- if as_json[:type] == "composite"
110
- as_json[:query] = as_json[:query].gsub(/%\{(.*?)\}/) do
111
- resolve_link($1, :monitor, id_map, **args)
115
+ case as_json[:type]
116
+ when "composite", "slo alert"
117
+ type = (as_json[:type] == "composite" ? :monitor : :slo)
118
+ as_json[:query] = as_json[:query].gsub(/%{(.*?)}/) do
119
+ resolve_link($1, type, id_map, **args) || $&
112
120
  end
113
121
  end
114
122
  end
@@ -129,6 +137,9 @@ module Kennel
129
137
 
130
138
  def self.normalize(expected, actual)
131
139
  super
140
+
141
+ ignore_default(expected, actual, MONITOR_DEFAULTS)
142
+
132
143
  options = actual.fetch(:options)
133
144
  options.delete(:silenced) # we do not manage silenced, so ignore it when diffing
134
145
 
@@ -65,19 +65,18 @@ module Kennel
65
65
 
66
66
  private
67
67
 
68
- def resolve_link(id, type, id_map, force:)
69
- value = id_map[id]
70
- if value == :new
68
+ def resolve_link(tracking_id, type, id_map, force:)
69
+ id = id_map[tracking_id]
70
+ if id == :new
71
71
  if force
72
- # TODO: remove the need for this by sorting monitors by missing resolutions
73
- invalid! "#{id} needs to already exist, try again"
72
+ invalid! "#{type} #{tracking_id} was referenced but is also created by the current run.\nIt could not be created because of a circular dependency, try creating only some of the resources"
74
73
  else
75
- id # will be re-resolved by syncer after the linked object was created
74
+ nil # will be re-resolved after the linked object was created
76
75
  end
77
- elsif value
78
- value
76
+ elsif id
77
+ id
79
78
  else
80
- invalid! "Unable to find #{type} #{id} (does not exist and is not being created by the current run)"
79
+ invalid! "Unable to find #{type} #{tracking_id} (does not exist and is not being created by the current run)"
81
80
  end
82
81
  end
83
82
 
@@ -69,7 +69,7 @@ module Kennel
69
69
  def resolve_linked_tracking_ids!(id_map, **args)
70
70
  return unless as_json[:monitor_ids] # ignore_default can remove it
71
71
  as_json[:monitor_ids] = as_json[:monitor_ids].map do |id|
72
- id.is_a?(String) ? resolve_link(id, :monitor, id_map, **args) : id
72
+ id.is_a?(String) ? (resolve_link(id, :monitor, id_map, **args) || id) : id
73
73
  end
74
74
  end
75
75
 
data/lib/kennel/syncer.rb CHANGED
@@ -38,23 +38,14 @@ module Kennel
38
38
  end
39
39
 
40
40
  def update
41
- changed = (@create + @update).map { |_, e| e }
42
-
43
- @create.each do |_, e|
44
- e.resolve_linked_tracking_ids!({}, force: true)
45
-
41
+ each_resolved @create do |_, e|
46
42
  reply = @api.create e.class.api_resource, e.as_json
47
43
  id = reply.fetch(:id)
48
-
49
- # resolve ids we could previously no resolve
50
- changed.delete e
51
- resolve_linked_tracking_ids! from: [reply], to: changed
52
-
44
+ populate_id_map [reply] # allow resolving ids we could previously no resolve
53
45
  Kennel.out.puts "Created #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
54
46
  end
55
47
 
56
- @update.each do |id, e|
57
- e.resolve_linked_tracking_ids!({}, force: true)
48
+ each_resolved @update do |id, e|
58
49
  @api.update e.class.api_resource, id, e.as_json
59
50
  Kennel.out.puts "Updated #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
60
51
  end
@@ -67,6 +58,37 @@ module Kennel
67
58
 
68
59
  private
69
60
 
61
+ # loop over items until everything is resolved or crash when we get stuck
62
+ # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
63
+ def each_resolved(list)
64
+ list = list.dup
65
+ loop do
66
+ return if list.empty?
67
+ list.reject! do |id, e|
68
+ if resolved?(e)
69
+ yield id, e
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end ||
75
+ assert_resolved(list[0][1]) # resolve something or show a circular dependency error
76
+ end
77
+ end
78
+
79
+ # TODO: optimize by storing an instance variable if already resolved
80
+ def resolved?(e)
81
+ assert_resolved e
82
+ true
83
+ rescue ValidationError
84
+ false
85
+ end
86
+
87
+ # raises ValidationError when not resolved
88
+ def assert_resolved(e)
89
+ resolve_linked_tracking_ids! [e], force: true
90
+ end
91
+
70
92
  def noop?
71
93
  @create.empty? && @update.empty? && @delete.empty?
72
94
  end
@@ -74,9 +96,15 @@ module Kennel
74
96
  def calculate_diff
75
97
  @update = []
76
98
  @delete = []
99
+ @id_map = {}
77
100
 
78
101
  actual = Progress.progress("Downloading definitions") { download_definitions }
79
- resolve_linked_tracking_ids! from: actual, to: @expected
102
+
103
+ # resolve dependencies to avoid diff
104
+ populate_id_map actual
105
+ @expected.each { |e| @id_map[e.tracking_id] ||= :new }
106
+ resolve_linked_tracking_ids! @expected
107
+
80
108
  filter_by_project! actual
81
109
 
82
110
  Progress.progress "Diffing" do
@@ -107,7 +135,6 @@ module Kennel
107
135
 
108
136
  ensure_all_ids_found
109
137
  @create = @expected.map { |e| [nil, e] }
110
- @create.sort_by! { |_, e| -DELETE_ORDER.index(e.class.api_resource) }
111
138
  end
112
139
 
113
140
  @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:api_resource) }
@@ -214,10 +241,12 @@ module Kennel
214
241
  end
215
242
  end
216
243
 
217
- def resolve_linked_tracking_ids!(from:, to:)
218
- map = from.each_with_object({}) { |a, lookup| lookup[tracking_id(a)] = a.fetch(:id) }
219
- to.each { |e| map[e.tracking_id] ||= :new }
220
- to.each { |e| e.resolve_linked_tracking_ids!(map, force: false) }
244
+ def populate_id_map(actual)
245
+ actual.each { |a| @id_map[tracking_id(a)] = a.fetch(:id) }
246
+ end
247
+
248
+ def resolve_linked_tracking_ids!(list, force: false)
249
+ list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
221
250
  end
222
251
 
223
252
  def filter_by_project!(definitions)
data/lib/kennel/utils.rb CHANGED
@@ -23,6 +23,11 @@ module Kennel
23
23
  .downcase
24
24
  end
25
25
 
26
+ # for child projects, not used internally
27
+ def title_case(string)
28
+ string.split(/[\s_]/).map(&:capitalize) * " "
29
+ end
30
+
26
31
  # simplified version of https://apidock.com/rails/ActiveSupport/Inflector/parameterize
27
32
  def parameterize(string)
28
33
  string
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.78.4"
3
+ VERSION = "1.81.2"
4
4
  end
data/template/Readme.md CHANGED
@@ -67,7 +67,7 @@ end
67
67
  - `cp .env.example .env`
68
68
  - open [Datadog API Settings](https://app.datadoghq.com/account/settings#api)
69
69
  - create a `API Key` or get an existing one from an admin, then add it to `.env` as `DATADOG_API_KEY`
70
- - find or create (check last page) your personal "Application Key" and add it to `.env` as `DATADOG_APP_KEY=`
70
+ - open [Datadog API Settings](https://app.datadoghq.com/access/application-keys) and create a new key, then add it to `.env` as `DATADOG_APP_KEY=`
71
71
  - change the `DATADOG_SUBDOMAIN=app` in `.env` to your companies subdomain if you have one
72
72
  - verify it works by running `rake plan`, it might show some diff, but should not crash
73
73
 
@@ -194,14 +194,20 @@ removing the `id` will cause kennel to create a new resource in datadog.
194
194
  Some validations might be too strict for your usecase or just wrong, please [open an issue](https://github.com/grosser/kennel/issues) and
195
195
  to unblock use the `validate: -> { false }` option.
196
196
 
197
- ### Linking with kennel_ids
197
+ ### Linking resources with kennel_id
198
198
 
199
- To link to existing monitors via their kennel_id `projects kennel_id` + `:` + `monitors kennel id`
199
+ Link resources with their kennel_id in the format `project kennel_id` + `:` + `resource kennel_id`,
200
+ this should be used to create dependent resources like monitor + slos,
201
+ so they can be created in a single update and can be re-created if any of them is deleted.
200
202
 
201
- - Screens `uptime` widgets can use `monitor: {id: "foo:bar"}`
202
- - Screens `alert_graph` widgets can use `alert_id: "foo:bar"`
203
- - Monitors `composite` can use `query: -> { "%{foo:bar} || %{foo:baz}" }`
204
- - Slos can use `monitor_ids: -> ["foo:bar"]`
203
+ |Resource|Type|Syntax|
204
+ |---|---|---|
205
+ |Dashboard|uptime|`monitor: {id: "foo:bar"}`|
206
+ |Dashboard|alert_graph|`alert_id: "foo:bar"`|
207
+ |Dashboard|slo|`slo_id: "foo:bar"`|
208
+ |Monitor|composite|`query: -> { "%{foo:bar} && %{foo:baz}" }`|
209
+ |Monitor|slo alert|`query: -> { "error_budget(\"%{foo:bar}\").over(\"7d\") > 123.0" }`|
210
+ |Slo|monitor|`monitor_ids: -> ["foo:bar"]`|
205
211
 
206
212
  ### Debugging changes locally
207
213
 
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.78.4
4
+ version: 1.81.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: 2020-12-17 00:00:00.000000000 Z
11
+ date: 2021-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday