Skip to content

Commit 6cca6e7

Browse files
lewhitjim80net
andauthored
feat: backup SLOs (#155)
* feat: backup SLOs (#1) * Add specs for slos * Add SLOs resource * Update spec/datadog_backup/slos_spec.rb Co-authored-by: Jim Park <jim@ramtank.com> * Update spec/datadog_backup/slos_spec.rb Co-authored-by: Jim Park <jim@ramtank.com> * Update spec/datadog_backup/slos_spec.rb Co-authored-by: Jim Park <jim@ramtank.com> * Go a level deeper for SLOs (do not need data header) --------- Co-authored-by: Jim Park <jim@ramtank.com>
1 parent 7ab85c1 commit 6cca6e7

File tree

4 files changed

+275
-1
lines changed

4 files changed

+275
-1
lines changed

bin/datadog_backup

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def prereqs(defaults) # rubocop:disable Metrics/AbcSize
4848
opts.on('--dashboards-only') do
4949
result[:resources] = [DatadogBackup::Dashboards]
5050
end
51+
opts.on('--slos-only') do
52+
result[:resources] = [DatadogBackup::SLOs]
53+
end
5154
opts.on('--synthetics-only') do
5255
result[:resources] = [DatadogBackup::Synthetics]
5356
end
@@ -83,7 +86,7 @@ defaults = {
8386
action: nil,
8487
backup_dir: File.join(ENV.fetch('PWD'), 'backup'),
8588
diff_format: :color,
86-
resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors, DatadogBackup::Synthetics],
89+
resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors, DatadogBackup::SLOs, DatadogBackup::Synthetics],
8790
output_format: :yaml,
8891
force_restore: false,
8992
disable_array_sort: false

lib/datadog_backup.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative 'datadog_backup/resources'
99
require_relative 'datadog_backup/dashboards'
1010
require_relative 'datadog_backup/monitors'
11+
require_relative 'datadog_backup/slos'
1112
require_relative 'datadog_backup/synthetics'
1213
require_relative 'datadog_backup/thread_pool'
1314
require_relative 'datadog_backup/version'

lib/datadog_backup/slos.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module DatadogBackup
4+
# SLO specific overrides for backup and restore.
5+
class SLOs < Resources
6+
def all
7+
get_all
8+
end
9+
10+
def backup
11+
LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads")
12+
futures = all.map do |slo|
13+
Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, slo) do |board|
14+
id = board[id_keyname]
15+
get_and_write_file(id)
16+
end
17+
end
18+
19+
watcher = ::DatadogBackup::ThreadPool.watcher
20+
watcher.join if watcher.status
21+
22+
Concurrent::Promises.zip(*futures).value!
23+
end
24+
25+
def get_by_id(id)
26+
begin
27+
slo = except(get(id))
28+
rescue Faraday::ResourceNotFound => e
29+
slo = {}
30+
end
31+
except(slo)
32+
end
33+
34+
def initialize(options)
35+
super(options)
36+
@banlist = %w[modified_at url].freeze
37+
end
38+
39+
# Return the Faraday body from a response with a 2xx status code, otherwise raise an error
40+
def body_with_2xx(response)
41+
unless response.status.to_s =~ /^2/
42+
raise "#{caller_locations(1,
43+
1)[0].label} failed with error #{response.status}"
44+
end
45+
46+
response.body.fetch('data')
47+
end
48+
49+
private
50+
51+
def api_version
52+
'v1'
53+
end
54+
55+
def api_resource_name
56+
'slo'
57+
end
58+
59+
def id_keyname
60+
'id'
61+
end
62+
end
63+
end

spec/datadog_backup/slos_spec.rb

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe DatadogBackup::SLOs do
6+
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
7+
let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } }
8+
let(:tempdir) { Dir.mktmpdir }
9+
let(:slos) do
10+
slos = described_class.new(
11+
action: 'backup',
12+
backup_dir: tempdir,
13+
output_format: :json,
14+
resources: []
15+
)
16+
allow(slos).to receive(:api_service).and_return(api_client_double)
17+
return slos
18+
end
19+
let(:fetched_slos) do
20+
{
21+
"data"=>[
22+
{"id"=>"abc-123", "name"=>"CI Stability", "tags"=>["kind:availability", "team:my_team"], "monitor_tags"=>[], "thresholds"=>[{"timeframe"=>"7d", "target"=>98.0, "target_display"=>"98."}, {"timeframe"=>"30d", "target"=>98.0, "target_display"=>"98."}, {"timeframe"=>"90d", "target"=>98.0, "target_display"=>"98."}], "type"=>"metric", "type_id"=>1, "description"=>"something helpful", "timeframe"=>"30d", "target_threshold"=>98.0, "query"=>{"denominator"=>"sum:metric.ci_things{*}.as_count()", "numerator"=>"sum:metric.ci_things{*}.as_count()-sum:metric.ci_things{infra_failure}.as_count()"}, "creator"=>{"name"=>"Thelma Patterson", "handle"=>"thelma.patterson@example.com", "email"=>"thelma.patterson@example.com"}, "created_at"=>1571335531, "modified_at"=>1687844157},
23+
{"id"=>"sbc-124", "name"=>"A Latency SLO", "tags"=>["team:my_team", "kind:latency"], "monitor_tags"=>[], "thresholds"=>[{"timeframe"=>"7d", "target"=>95.0, "target_display"=>"95."}, {"timeframe"=>"30d", "target"=>95.0, "target_display"=>"95."}, {"timeframe"=>"90d", "target"=>95.0, "target_display"=>"95."}], "type"=>"monitor", "type_id"=>0, "description"=>"", "timeframe"=>"30d", "target_threshold"=>95.0, "monitor_ids"=>[13158755], "creator"=>{"name"=>"Louise Montague", "handle"=>"louise.montague@example.com", "email"=>"louise.montague@example.com"}, "created_at"=>1573162531, "modified_at"=>1685819875}
24+
],
25+
"errors"=>[],
26+
"metadata"=>{"page"=>{"total_count"=>359, "total_filtered_count"=>359}}
27+
}
28+
end
29+
let(:slo_abc_123) do
30+
{
31+
"id" => "abc-123",
32+
"name" => "CI Stability",
33+
"tags" => [
34+
"kind:availability",
35+
"team:my_team",
36+
],
37+
"monitor_tags" => [],
38+
"thresholds" => [
39+
{
40+
"timeframe" => "7d",
41+
"target" => 98.0,
42+
"target_display" => "98."
43+
},
44+
{
45+
"timeframe" => "30d",
46+
"target" => 98.0,
47+
"target_display" => "98."
48+
},
49+
{
50+
"timeframe" => "90d",
51+
"target" => 98.0,
52+
"target_display" => "98."
53+
}
54+
],
55+
"type" => "metric",
56+
"type_id" => 1,
57+
"description" => "something helpful",
58+
"timeframe" => "30d",
59+
"target_threshold" => 98.0,
60+
"query" => {
61+
"denominator" => "sum:metric.ci_things{*}.as_count()",
62+
"numerator" => "sum:metric.ci_things{*}.as_count()-sum:metric.ci_things{infra_failure}.as_count()"
63+
},
64+
"creator" => {
65+
"name" => "Thelma Patterson",
66+
"handle" => "thelma.patterson@example.com",
67+
"email" => "thelma.patterson@example.com"
68+
},
69+
"created_at" => 1571335531,
70+
"modified_at" => 1687844157
71+
}
72+
end
73+
let(:slo_sbc_124) do
74+
{
75+
"id" => "sbc-124",
76+
"name" => "A Latency SLO",
77+
"tags" => [
78+
"kind:latency",
79+
"team:my_team",
80+
],
81+
"monitor_tags" => [],
82+
"thresholds" => [
83+
{
84+
"timeframe" => "7d",
85+
"target" => 98.0,
86+
"target_display" => "98."
87+
},
88+
{
89+
"timeframe" => "30d",
90+
"target" => 98.0,
91+
"target_display" => "98."
92+
},
93+
{
94+
"timeframe" => "90d",
95+
"target" => 98.0,
96+
"target_display" => "98."
97+
}
98+
],
99+
"type" => "monitor",
100+
"type_id"=>0,
101+
"description"=>"",
102+
"timeframe"=>"30d",
103+
"target_threshold"=>95.0,
104+
"monitor_ids"=>[ 13158755 ],
105+
"creator"=>{
106+
"name"=>"Louise Montague",
107+
"handle"=>"louise.montague@example.com",
108+
"email"=>"louise.montague@example.com"
109+
},
110+
"created_at"=>1573162531,
111+
"modified_at"=>1685819875
112+
}
113+
end
114+
let(:slo_abc_123_response) do
115+
{ "data" => slo_abc_123, "errors" => [] }
116+
end
117+
let(:slo_sbc_124_response) do
118+
{ "data" => slo_sbc_124, "errors" => [] }
119+
end
120+
let(:all_slos) { respond_with200(fetched_slos) }
121+
let(:example_slo1) { respond_with200(slo_abc_123_response) }
122+
let(:example_slo2) { respond_with200(slo_sbc_124_response) }
123+
124+
before do
125+
stubs.get('/api/v1/slo') { all_slos }
126+
stubs.get('/api/v1/slo/abc-123') { example_slo1 }
127+
stubs.get('/api/v1/slo/sbc-124') { example_slo2 }
128+
end
129+
130+
describe '#backup' do
131+
subject { slos.backup }
132+
133+
it 'is expected to create two files' do
134+
file1 = instance_double(File)
135+
allow(File).to receive(:open).with(slos.filename('abc-123'), 'w').and_return(file1)
136+
allow(file1).to receive(:write)
137+
allow(file1).to receive(:close)
138+
139+
file2 = instance_double(File)
140+
allow(File).to receive(:open).with(slos.filename('sbc-124'), 'w').and_return(file2)
141+
allow(file2).to receive(:write)
142+
allow(file2).to receive(:close)
143+
144+
slos.backup
145+
expect(file1).to have_received(:write).with(::JSON.pretty_generate(slo_abc_123.deep_sort))
146+
expect(file2).to have_received(:write).with(::JSON.pretty_generate(slo_sbc_124.deep_sort))
147+
end
148+
end
149+
150+
describe '#filename' do
151+
subject { slos.filename('abc-123') }
152+
153+
it { is_expected.to eq("#{tempdir}/slos/abc-123.json") }
154+
end
155+
156+
describe '#get_by_id' do
157+
subject { slos.get_by_id('abc-123') }
158+
159+
it { is_expected.to eq slo_abc_123 }
160+
end
161+
162+
describe '#diff' do
163+
it 'calls the api only once' do
164+
slos.write_file('{"a":"b"}', slos.filename('abc-123'))
165+
expect(slos.diff('abc-123')).to eq(<<~EODASH
166+
---
167+
-created_at: 1571335531
168+
-creator:
169+
- email: thelma.patterson@example.com
170+
- handle: thelma.patterson@example.com
171+
- name: Thelma Patterson
172+
-description: something helpful
173+
-id: abc-123
174+
-monitor_tags: []
175+
-name: CI Stability
176+
-query:
177+
- denominator: sum:metric.ci_things{*}.as_count()
178+
- numerator: sum:metric.ci_things{*}.as_count()-sum:metric.ci_things{infra_failure}.as_count()
179+
-tags:
180+
-- kind:availability
181+
-- team:my_team
182+
-target_threshold: 98.0
183+
-thresholds:
184+
-- target: 98.0
185+
- target_display: '98.'
186+
- timeframe: 30d
187+
-- target: 98.0
188+
- target_display: '98.'
189+
- timeframe: 7d
190+
-- target: 98.0
191+
- target_display: '98.'
192+
- timeframe: 90d
193+
-timeframe: 30d
194+
-type: metric
195+
-type_id: 1
196+
+a: b
197+
EODASH
198+
.chomp)
199+
end
200+
end
201+
202+
describe '#except' do
203+
subject { slos.except({ :a => :b, 'modified_at' => :c, 'url' => :d }) }
204+
205+
it { is_expected.to eq({ a: :b }) }
206+
end
207+
end

0 commit comments

Comments
 (0)