kennel 1.89.0 → 1.89.1

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: 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