kennel 1.89.0 → 1.89.1

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: bdedd0f9bbcb9abf873cd2f9be0414700f77d2ea864e609db79773d1b3193e8e
4
- data.tar.gz: 336875475665fac019c12daced64e474fb8e85e216e158d4328b62b287785e6a
3
+ metadata.gz: ba1d58935b541b89cb78318964470bb209933a491d9b81e23785de0fb6ab2654
4
+ data.tar.gz: aed4b13dc2c35ced80073e592fd0d2ba2afda8dfb763d2c6baa4b1f30851cf53
5
5
  SHA512:
6
- metadata.gz: 0041e466ce433d82f9db8252829f8c64652815d7bf932ec93596d57af76dc13aaa9dfce963bbf82888e09d424937b597364954edc1cff14344143f0661e8e274
7
- data.tar.gz: a286f227e597686085049ef0227b2e36b26d33ef41a7d96d13a535af73e1530254de6bf68ead11ebdd0c394d1562766a91a478c794ee17f35c288a7f102d46b7
6
+ metadata.gz: d3595608df6e40ec4241bf2b758f1ed1b3fde0e63dff7f5a4fa393a358cf9cb9fa33bb0c5f72899b7df54817cb9482366d9c387e3c21a33249e35e3bcc4fc8ea
7
+ data.tar.gz: 745ec8c762acc885a9d0f2ae23fc9f43ca05f56b8c580501075e2b9237e7199a09499053cebb45e5bfa477aa0a187cabf5899e05f7bf6c361f952347ab6cac34
data/Readme.md CHANGED
@@ -52,6 +52,7 @@ end
52
52
  ```
53
53
 
54
54
  <!-- NOT IN template/Readme.md -->
55
+
55
56
  ## Installation
56
57
 
57
58
  - create a new private `kennel` repo for your organization (do not fork this repo)
@@ -293,9 +294,12 @@ https://foo.datadog.com/monitor/123
293
294
 
294
295
  <!-- NOT IN template/Readme.md -->
295
296
 
296
-
297
297
  ## Development
298
298
 
299
+ ### Benchmarking
300
+
301
+ Setting `FORCE_GET_CACHE=true` will cache all get requests, which makes benchmarking improvements more reliable.
302
+
299
303
  ### Integration testing
300
304
 
301
305
  ```Bash
data/lib/kennel/api.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ # encapsulates knowledge around how the api works
3
+ # especially 1-off weirdness that should not lak into other parts of the code
2
4
  module Kennel
3
- # encapsulates knowledge around how the api works
4
5
  class Api
5
6
  CACHE_FILE = "tmp/cache/details"
6
7
 
@@ -11,34 +12,24 @@ module Kennel
11
12
  end
12
13
 
13
14
  def show(api_resource, id, params = {})
14
- reply = request :get, "/api/v1/#{api_resource}/#{id}", params: params
15
- api_resource == "slo" ? reply[:data] : reply
15
+ response = request :get, "/api/v1/#{api_resource}/#{id}", params: params
16
+ response = response.fetch(:data) if api_resource == "slo"
17
+ response
16
18
  end
17
19
 
18
20
  def list(api_resource, params = {})
19
- if api_resource == "slo"
20
- raise ArgumentError if params[:limit] || params[:offset]
21
- limit = 1000
22
- offset = 0
23
- all = []
24
-
25
- loop do
26
- result = request :get, "/api/v1/#{api_resource}", params: params.merge(limit: limit, offset: offset)
27
- data = result.fetch(:data)
28
- all.concat data
29
- break all if data.size < limit
30
- offset += limit
31
- end
32
- else
33
- result = request :get, "/api/v1/#{api_resource}", params: params
34
- result = result.fetch(:dashboards) if api_resource == "dashboard"
35
- result
21
+ with_pagination api_resource == "slo", params do |paginated_params|
22
+ response = request :get, "/api/v1/#{api_resource}", params: paginated_params
23
+ response = response.fetch(:dashboards) if api_resource == "dashboard"
24
+ response = response.fetch(:data) if api_resource == "slo"
25
+ response
36
26
  end
37
27
  end
38
28
 
39
29
  def create(api_resource, attributes)
40
- reply = request :post, "/api/v1/#{api_resource}", body: attributes
41
- api_resource == "slo" ? reply[:data].first : reply
30
+ response = request :post, "/api/v1/#{api_resource}", body: attributes
31
+ response = response.fetch(:data).first if api_resource == "slo"
32
+ response
42
33
  end
43
34
 
44
35
  def update(api_resource, id, attributes)
@@ -53,7 +44,6 @@ module Kennel
53
44
  end
54
45
 
55
46
  def fill_details!(api_resource, list)
56
- return unless api_resource == "dashboard"
57
47
  details_cache do |cache|
58
48
  Utils.parallel(list) { |a| fill_detail!(api_resource, a, cache) }
59
49
  end
@@ -61,6 +51,21 @@ module Kennel
61
51
 
62
52
  private
63
53
 
54
+ def with_pagination(enabled, params)
55
+ return yield params unless enabled
56
+ raise ArgumentError if params[:limit] || params[:offset]
57
+ limit = 1000
58
+ offset = 0
59
+ all = []
60
+
61
+ loop do
62
+ response = yield params.merge(limit: limit, offset: offset)
63
+ all.concat response
64
+ return all if response.size < limit
65
+ offset += limit
66
+ end
67
+ end
68
+
64
69
  # Make diff work even though we cannot mass-fetch definitions
65
70
  def fill_detail!(api_resource, a, cache)
66
71
  args = [api_resource, a.fetch(:id)]
@@ -74,34 +79,52 @@ module Kennel
74
79
  end
75
80
 
76
81
  def request(method, path, body: nil, params: {}, ignore_404: false)
77
- params = params.merge(application_key: @app_key, api_key: @api_key)
78
- query = Faraday::FlatParamsEncoder.encode(params)
79
- response = nil
80
- tries = 2
81
-
82
- tries.times do |i|
83
- response = Utils.retry Faraday::ConnectionFailed, Faraday::TimeoutError, times: 2 do
84
- @client.send(method, "#{path}?#{query}") do |request|
85
- request.body = JSON.generate(body) if body
86
- request.headers["Content-type"] = "application/json"
82
+ path = "#{path}?#{Faraday::FlatParamsEncoder.encode(params)}" if params.any?
83
+ with_cache ENV["FORCE_GET_CACHE"] && method == :get, path do
84
+ response = nil
85
+ tries = 2
86
+
87
+ tries.times do |i|
88
+ response = Utils.retry Faraday::ConnectionFailed, Faraday::TimeoutError, times: 2 do
89
+ @client.send(method, path) do |request|
90
+ request.body = JSON.generate(body) if body
91
+ request.headers["Content-type"] = "application/json"
92
+ request.headers["DD-API-KEY"] = @api_key
93
+ request.headers["DD-APPLICATION-KEY"] = @app_key
94
+ end
87
95
  end
96
+
97
+ break if i == tries - 1 || method != :get || response.status < 500
98
+ Kennel.err.puts "Retrying on server error #{response.status} for #{path}"
88
99
  end
89
100
 
90
- break if i == tries - 1 || method != :get || response.status < 500
91
- Kennel.err.puts "Retrying on server error #{response.status} for #{path}"
92
- end
101
+ if !response.success? && (response.status != 404 || !ignore_404)
102
+ message = +"Error #{response.status} during #{method.upcase} #{path}\n"
103
+ message << "request:\n#{JSON.pretty_generate(body)}\nresponse:\n" if body
104
+ message << response.body
105
+ raise message
106
+ end
93
107
 
94
- if !response.success? && (response.status != 404 || !ignore_404)
95
- message = +"Error #{response.status} during #{method.upcase} #{path}\n"
96
- message << "request:\n#{JSON.pretty_generate(body)}\nresponse:\n" if body
97
- message << response.body
98
- raise message
108
+ if response.body.empty?
109
+ {}
110
+ else
111
+ JSON.parse(response.body, symbolize_names: true)
112
+ end
99
113
  end
114
+ end
100
115
 
101
- if response.body.empty?
102
- {}
116
+ # allow caching all requests to speedup/benchmark logic that includes repeated requests
117
+ def with_cache(enabled, key)
118
+ return yield unless enabled
119
+ dir = "tmp/cache"
120
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
121
+ file = "#{dir}/#{key.delete("/?=")}" # TODO: encode nicely
122
+ if File.exist?(file)
123
+ Marshal.load(File.read(file)) # rubocop:disable Security/MarshalLoad
103
124
  else
104
- JSON.parse(response.body, symbolize_names: true)
125
+ result = yield
126
+ File.write(file, Marshal.dump(result))
127
+ result
105
128
  end
106
129
  end
107
130
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cache that reads everything from a single file
4
- # to avoid doing multiple disk reads while iterating all definitions
5
- # it also replaces updated keys and has an overall expiry to not keep deleted things forever
4
+ # - avoids doing multiple disk reads while iterating all definitions
5
+ # - has a global expiry to not keep deleted resources forever
6
6
  module Kennel
7
7
  class FileCache
8
8
  def initialize(file, cache_version)
@@ -22,10 +22,11 @@ module Kennel
22
22
 
23
23
  def fetch(key, key_version)
24
24
  old_value, old_version = @data[key]
25
- return old_value if old_version == [key_version, @cache_version]
25
+ expected_version = [key_version, @cache_version]
26
+ return old_value if old_version == expected_version
26
27
 
27
28
  new_value = yield
28
- @data[key] = [new_value, [key_version, @cache_version], @expires]
29
+ @data[key] = [new_value, expected_version, @expires]
29
30
  new_value
30
31
  end
31
32
 
@@ -46,8 +47,11 @@ module Kennel
46
47
  File.write(@file, Marshal.dump(@data))
47
48
  end
48
49
 
50
+ # keep the cache small to make loading it fast (5MB ~= 100ms)
51
+ # - delete expired keys
52
+ # - delete what would be deleted anyway when updating
49
53
  def expire_old_data
50
- @data.reject! { |_, (_, _, ex)| ex < @now }
54
+ @data.reject! { |_, (_, (_, cv), expires)| expires < @now || cv != @cache_version }
51
55
  end
52
56
  end
53
57
  end
@@ -113,8 +113,9 @@ module Kennel
113
113
  next if request[:formulas] && request[:formulas] != [{ formula: "query1" }]
114
114
  next if request[:queries]&.size != 1
115
115
  next if request[:queries].any? { |q| q[:data_source] != "metrics" }
116
+ next if widget.dig(:definition, :type) != request[:response_format]
116
117
  request.delete(:formulas)
117
- request[:type] = request.delete(:response_format)
118
+ request.delete(:response_format)
118
119
  request[:q] = request.delete(:queries).first.fetch(:query)
119
120
  end
120
121
  end
@@ -6,7 +6,7 @@ module Kennel
6
6
  TRACKING_FIELDS = [:message, :description].freeze
7
7
  READONLY_ATTRIBUTES = [
8
8
  :deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at,
9
- :klass # added by syncer.rb
9
+ :klass, :tracking_id # added by syncer.rb
10
10
  ].freeze
11
11
 
12
12
  settings :id, :kennel_id
@@ -29,6 +29,8 @@ module Kennel
29
29
  Kennel.err.print "#{time.round(2)}s\n"
30
30
 
31
31
  result
32
+ ensure
33
+ stop = true
32
34
  end
33
35
  end
34
36
  end
data/lib/kennel/syncer.rb CHANGED
@@ -2,21 +2,12 @@
2
2
  module Kennel
3
3
  class Syncer
4
4
  DELETE_ORDER = ["dashboard", "slo", "monitor"].freeze # dashboards references monitors + slos, slos reference monitors
5
- LINE_UP = "\e[1A"
5
+ LINE_UP = "\e[1A\033[K" # go up and clear
6
6
 
7
7
  def initialize(api, expected, project: nil)
8
8
  @api = api
9
9
  @project_filter = project
10
10
  @expected = expected
11
- if @project_filter
12
- original = @expected
13
- @expected = @expected.select { |e| e.project.kennel_id == @project_filter }
14
- if @expected.empty?
15
- possible = original.map { |e| e.project.kennel_id }.uniq.sort
16
- raise "#{@project_filter} does not match any projects, try any of these:\n#{possible.join("\n")}"
17
- end
18
- end
19
- @expected.each(&:add_tracking_id)
20
11
  calculate_diff
21
12
  prevent_irreversible_partial_updates
22
13
  end
@@ -41,9 +32,9 @@ module Kennel
41
32
  message = "#{e.class.api_resource} #{e.tracking_id}"
42
33
  Kennel.out.puts "Creating #{message}"
43
34
  reply = @api.create e.class.api_resource, e.as_json
44
- reply[:klass] = e.class # store api resource class for later use
35
+ cache_metadata reply, e.class
45
36
  id = reply.fetch(:id)
46
- populate_id_map [reply] # allow resolving ids we could previously no resolve
37
+ populate_id_map [], [reply] # allow resolving ids we could previously no resolve
47
38
  Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
48
39
  end
49
40
 
@@ -56,8 +47,7 @@ module Kennel
56
47
 
57
48
  @delete.each do |id, _, a|
58
49
  klass = a.fetch(:klass)
59
- tracking_id = klass.parse_tracking_id(a)
60
- message = "#{klass.api_resource} #{tracking_id} #{id}"
50
+ message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
61
51
  Kennel.out.puts "Deleting #{message}"
62
52
  @api.delete klass.api_resource, id
63
53
  Kennel.out.puts "#{LINE_UP}Deleted #{message}"
@@ -108,14 +98,14 @@ module Kennel
108
98
 
109
99
  actual = Progress.progress("Downloading definitions") { download_definitions }
110
100
 
111
- # resolve dependencies to avoid diff
112
- populate_id_map actual
113
- @expected.each { |e| @id_map[e.tracking_id] ||= :new }
114
- resolve_linked_tracking_ids! @expected
101
+ Progress.progress "Diffing" do
102
+ filter_expected_by_project! @expected
103
+ populate_id_map @expected, actual
104
+ filter_actual_by_project! actual
105
+ resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
115
106
 
116
- filter_by_project! actual
107
+ @expected.each(&:add_tracking_id) # avoid diff with actual
117
108
 
118
- Progress.progress "Diffing" do
119
109
  items = actual.map do |a|
120
110
  e = matching_expected(a)
121
111
  if e && @expected.delete(e)
@@ -126,9 +116,8 @@ module Kennel
126
116
  end
127
117
 
128
118
  # fill details of things we need to compare
129
- detailed = Hash.new { |h, k| h[k] = [] }
130
- items.each { |e, a| detailed[a[:klass]] << a if e }
131
- detailed.each { |klass, actuals| @api.fill_details! klass.api_resource, actuals }
119
+ details = items.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
120
+ @api.fill_details! "dashboard", details
132
121
 
133
122
  # pick out things to update or delete
134
123
  items.each do |e, a|
@@ -136,26 +125,30 @@ module Kennel
136
125
  if e
137
126
  diff = e.diff(a)
138
127
  @update << [id, e, a, diff] if diff.any?
139
- elsif a.fetch(:klass).parse_tracking_id(a) # was previously managed
128
+ elsif a.fetch(:tracking_id) # was previously managed
140
129
  @delete << [id, nil, a]
141
130
  end
142
131
  end
143
132
 
144
133
  ensure_all_ids_found
145
134
  @create = @expected.map { |e| [nil, e] }
135
+ @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
146
136
  end
147
-
148
- @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
149
137
  end
150
138
 
151
139
  def download_definitions
152
140
  Utils.parallel(Models::Record.subclasses) do |klass|
153
141
  results = @api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
154
142
  results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
155
- results.each { |c| c[:klass] = klass } # store api resource for later diffing
143
+ results.each { |a| cache_metadata(a, klass) }
156
144
  end.flatten(1)
157
145
  end
158
146
 
147
+ def cache_metadata(a, klass)
148
+ a[:klass] = klass
149
+ a[:tracking_id] = a.fetch(:klass).parse_tracking_id(a)
150
+ end
151
+
159
152
  def ensure_all_ids_found
160
153
  @expected.each do |e|
161
154
  next unless id = e.id
@@ -176,14 +169,14 @@ module Kennel
176
169
  end
177
170
 
178
171
  klass = a.fetch(:klass)
179
- @lookup_map["#{klass.api_resource}:#{a.fetch(:id)}"] || @lookup_map[klass.parse_tracking_id(a)]
172
+ @lookup_map["#{klass.api_resource}:#{a.fetch(:id)}"] || @lookup_map[a.fetch(:tracking_id)]
180
173
  end
181
174
 
182
175
  def print_plan(step, list, color)
183
176
  return if list.empty?
184
177
  list.each do |_, e, a, diff|
185
178
  klass = (e ? e.class : a.fetch(:klass))
186
- Kennel.out.puts Utils.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || klass.parse_tracking_id(a)}")
179
+ Kennel.out.puts Utils.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
187
180
  print_diff(diff) if diff # only for update
188
181
  end
189
182
  end
@@ -232,19 +225,34 @@ module Kennel
232
225
  end
233
226
  end
234
227
 
235
- def populate_id_map(actual)
236
- actual.each { |a| @id_map[a.fetch(:klass).parse_tracking_id(a)] = a.fetch(:id) }
228
+ def populate_id_map(expected, actual)
229
+ actual.each do |a|
230
+ next unless tracking_id = a.fetch(:tracking_id)
231
+ @id_map[tracking_id] = a.fetch(:id)
232
+ end
233
+ expected.each { |e| @id_map[e.tracking_id] ||= :new }
237
234
  end
238
235
 
239
236
  def resolve_linked_tracking_ids!(list, force: false)
240
237
  list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
241
238
  end
242
239
 
243
- def filter_by_project!(definitions)
240
+ def filter_actual_by_project!(actual)
241
+ return unless @project_filter
242
+ actual.select! do |a|
243
+ tracking_id = a.fetch(:tracking_id)
244
+ !tracking_id || tracking_id.start_with?("#{@project_filter}:")
245
+ end
246
+ end
247
+
248
+ def filter_expected_by_project!(expected)
244
249
  return unless @project_filter
245
- definitions.select! do |a|
246
- id = a.fetch(:klass).parse_tracking_id(a)
247
- !id || id.start_with?("#{@project_filter}:")
250
+ original = expected.dup
251
+ expected.select! { |e| e.project.kennel_id == @project_filter }
252
+
253
+ if expected.empty?
254
+ possible = original.map { |e| e.project.kennel_id }.uniq.sort
255
+ raise "#{@project_filter} does not match any projects, try any of these:\n#{possible.join("\n")}"
248
256
  end
249
257
  end
250
258
  end
data/lib/kennel/tasks.rb CHANGED
@@ -138,7 +138,7 @@ namespace :kennel do
138
138
  resources.each do |resource|
139
139
  Kennel::Progress.progress("Downloading #{resource}") do
140
140
  list = api.list(resource)
141
- api.fill_details!(resource, list)
141
+ api.fill_details!(resource, list) if resource == "dashboard"
142
142
  end
143
143
  list.each do |r|
144
144
  r[:api_resource] = resource
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.89.0"
3
+ VERSION = "1.89.1"
4
4
  end
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.89.0
4
+ version: 1.89.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-25 00:00:00.000000000 Z
11
+ date: 2021-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday