dogtrainer 0.1.0
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 +7 -0
- data/.gitignore +37 -0
- data/.rubocop.yml +27 -0
- data/ChangeLog.md +3 -0
- data/Gemfile +2 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/Rakefile +46 -0
- data/circle.yml +22 -0
- data/dogtrainer.gemspec +59 -0
- data/lib/dogtrainer.rb +6 -0
- data/lib/dogtrainer/api.rb +437 -0
- data/lib/dogtrainer/logging.rb +85 -0
- data/lib/dogtrainer/version.rb +4 -0
- data/spec/spec_helper.rb +57 -0
- data/spec/unit/api_spec.rb +1229 -0
- data/spec/unit/logging_spec.rb +80 -0
- metadata +423 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6b195d6ba08f5c8305b7bed4f4aaac84f24e678e
|
4
|
+
data.tar.gz: 75d793bc0c14824a1c52b8155684a9cb5ab9b3e1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1ded76cc831e7c7d291db142fc5233362ce5d42afb05e05c448422eba521d122541c64d797fd6920175bc5ef54711e1fc5f7207d6b5825473a7f150026f1d9af
|
7
|
+
data.tar.gz: d42b5f8d2e501e06f7d40eb297316d85cd295c80a8a89098aff268484d5d6a12e85374123200bf40760575c2a1439a33b3147c62a51a721c4c2aa21dc1a387d3
|
data/.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
/results/
|
12
|
+
/vendor/
|
13
|
+
|
14
|
+
## Specific to RubyMotion:
|
15
|
+
.dat*
|
16
|
+
.repl_history
|
17
|
+
build/
|
18
|
+
|
19
|
+
## Documentation cache and generated files:
|
20
|
+
/.yardoc/
|
21
|
+
/_yardoc/
|
22
|
+
/doc/
|
23
|
+
/rdoc/
|
24
|
+
|
25
|
+
## Environment normalisation:
|
26
|
+
/.bundle/
|
27
|
+
/vendor/bundle
|
28
|
+
/lib/bundler/man/
|
29
|
+
|
30
|
+
# for a library or gem, you might want to ignore these files since the code is
|
31
|
+
# intended to run in multiple environments; otherwise, check them in:
|
32
|
+
Gemfile.lock
|
33
|
+
# .ruby-version
|
34
|
+
# .ruby-gemset
|
35
|
+
|
36
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
37
|
+
.rvmrc
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#Lint/UselessAssignment:
|
2
|
+
# Exclude:
|
3
|
+
# - 'lib/dogtrainer/api.rb'
|
4
|
+
# - 'spec/unit/api_spec.rb'
|
5
|
+
|
6
|
+
Metrics/AbcSize:
|
7
|
+
Max: 50
|
8
|
+
|
9
|
+
Metrics/ClassLength:
|
10
|
+
Max: 300
|
11
|
+
|
12
|
+
Metrics/CyclomaticComplexity:
|
13
|
+
Max: 10
|
14
|
+
|
15
|
+
Metrics/MethodLength:
|
16
|
+
Max: 50
|
17
|
+
|
18
|
+
Metrics/PerceivedComplexity:
|
19
|
+
Max: 20
|
20
|
+
|
21
|
+
Style/AccessorMethodName:
|
22
|
+
Exclude:
|
23
|
+
- 'lib/dogtrainer/api.rb'
|
24
|
+
|
25
|
+
Style/PreferredHashMethods:
|
26
|
+
Exclude:
|
27
|
+
- 'lib/dogtrainer/api.rb'
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
guard :bundler do
|
2
|
+
watch('dogtrainer.gemspec')
|
3
|
+
end
|
4
|
+
|
5
|
+
guard :rubocop do
|
6
|
+
watch(/.+\.rb$/)
|
7
|
+
watch(%r{(?:.+\/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
8
|
+
end
|
9
|
+
|
10
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
11
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
12
|
+
watch(%r{^spec/.+_spec\.rb})
|
13
|
+
watch(%r{^spec/(.+)/.+_spec\.rb})
|
14
|
+
watch(%r{^lib\/dogtrainer/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
15
|
+
end
|
16
|
+
|
17
|
+
guard 'yard', port: '8808' do
|
18
|
+
watch(/README\.md/)
|
19
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Manheim
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# dogtrainer
|
2
|
+
|
3
|
+
Build of master branch: [](https://circleci.com/gh/manheim/dogtrainer)
|
4
|
+
|
5
|
+
Documentation: [http://www.rubydoc.info/gems/dogtrainer/](http://www.rubydoc.info/gems/dogtrainer/)
|
6
|
+
|
7
|
+
Wrapper around DataDog dogapi gem to simplify creation and management of Monitors and Boards.
|
8
|
+
|
9
|
+
This class provides methods to manage (upsert / ensure the existence and configuration of) DataDog
|
10
|
+
Monitors and TimeBoards/ScreenBoards.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
To use the helper class, add ``dogtrainer`` as a runtime dependency for your project.
|
15
|
+
|
16
|
+
Using the best-practice of declaring all of your dependencies in your ``gemspec``:
|
17
|
+
|
18
|
+
``Gemfile``:
|
19
|
+
|
20
|
+
```
|
21
|
+
source 'https://rubygems.org'
|
22
|
+
gemspec
|
23
|
+
```
|
24
|
+
|
25
|
+
And add in your ``.gemspec``:
|
26
|
+
|
27
|
+
```
|
28
|
+
gem.add_runtime_dependency 'dogtrainer'
|
29
|
+
```
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
To use the DataDog helper, require the module and create an instance of the class,
|
34
|
+
passing it the required configuration information.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'dogtrainer'
|
38
|
+
|
39
|
+
dog = DogTrainer::API.new(api_key, app_key, notify_to)
|
40
|
+
```
|
41
|
+
|
42
|
+
or
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'dogtrainer'
|
46
|
+
|
47
|
+
dog = DogTrainer::API.new(api_key, app_key, notify_to, 'string describing where to update monitors or boards')
|
48
|
+
```
|
49
|
+
|
50
|
+
* __api_key__ is your DataDog API Key, which you can find at https://app.datadoghq.com/account/settings#api
|
51
|
+
* __app_key__ is an application-specific key, which should be generated separately for every app or
|
52
|
+
service that uses this class. These can be generated and seen at https://app.datadoghq.com/account/settings#api
|
53
|
+
* __notify_to__ is the string specifying DataDog monitor recipients in "@" form. If you are only managing Timeboards or
|
54
|
+
Screenboards (not Monitors), this can be ``nil``.
|
55
|
+
* __repo_path__ is a string that will be included in all Monitor notification messages and TimeBoard/ScreenBoard descriptions,
|
56
|
+
telling users where to find the code that created the DataDog resource. This is intended to alert users to code-managed
|
57
|
+
items that shouldn't be manually changed. If this parameter is not specified, it will be obtained from the first usable
|
58
|
+
and present value of: the ``GIT_URL`` environment variable, the ``CIRCLE_REPOSITORY_URL`` or the first remote URL found
|
59
|
+
by running ``git config --local -l`` in the directory that contains the code calling this constructor.
|
60
|
+
|
61
|
+
### Usage Examples
|
62
|
+
|
63
|
+
These examples all rely on the ``require`` and class instantiation above.
|
64
|
+
|
65
|
+
#### Monitors
|
66
|
+
|
67
|
+
Event alert on sparse data (lack of data should not trigger an alert):
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# alert if more than 130 EC2 RunInstances API call Events in the last hour;
|
71
|
+
# do not alert if there is no data.
|
72
|
+
id = dog.upsert_monitor(
|
73
|
+
"AWS Suspicious Activity - EC2 RunInstances in the past hour",
|
74
|
+
"events('sources:cloudtrail priority:all tags:runinstances').rollup('count').last('1h') > 130",
|
75
|
+
130,
|
76
|
+
'<=',
|
77
|
+
alert_no_data: false,
|
78
|
+
mon_type: 'event alert'
|
79
|
+
)
|
80
|
+
puts "RunInstances monitor id: #{id}"
|
81
|
+
```
|
82
|
+
|
83
|
+
Metric alert, ignoring sparse data:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# alert if aws.ec2.host_ok metric is > 2000 in the last hour, ignoring sparse data
|
87
|
+
id = dog.upsert_monitor(
|
88
|
+
"AWS Suspicious Activity - Average Running (OK) EC2 Instances in the past hour",
|
89
|
+
"avg(last_1h):sum:aws.ec2.host_ok{*} > 2000",
|
90
|
+
2000,
|
91
|
+
'<=',
|
92
|
+
alert_no_data: false,
|
93
|
+
mon_type: 'metric alert'
|
94
|
+
)
|
95
|
+
puts "aws.ec2.host_ok monitor id: #{id}"
|
96
|
+
```
|
97
|
+
|
98
|
+
Metric alert, also alerting on missing data:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# alert if 'MY_ASG_NAME' ASG in service instances < 2
|
102
|
+
dog.upsert_monitor(
|
103
|
+
"ASG In-Service Instances",
|
104
|
+
"avg(last_5m):sum:aws.autoscaling.group_in_service_instances{autoscaling_group:MY_ASG_NAME} < 2",
|
105
|
+
2,
|
106
|
+
'>='
|
107
|
+
)
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Boards
|
111
|
+
|
112
|
+
Create a TimeBoard with a handful of graphs about the "MY_ELB_NAME" ELB,
|
113
|
+
"MY_ASG_NAME" ASG and instances tagged with a Name of "MY_INSTANCE":
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
asg_name = "MY_ASG_NAME"
|
117
|
+
elb_name = "MY_ELB_NAME"
|
118
|
+
instance_name = "MY_INSTANCE"
|
119
|
+
|
120
|
+
# generate graph definitions
|
121
|
+
graphs = [
|
122
|
+
dog.graphdef(
|
123
|
+
"ASG In-Service Instances",
|
124
|
+
[
|
125
|
+
"sum:aws.autoscaling.group_in_service_instances{autoscaling_group:#{asg_name}}",
|
126
|
+
"sum:aws.autoscaling.group_desired_capacity{autoscaling_group:#{asg_name}}"
|
127
|
+
]
|
128
|
+
),
|
129
|
+
dog.graphdef(
|
130
|
+
"ELB Healthy Hosts Sum",
|
131
|
+
"sum:aws.elb.healthy_host_count_deduped{host:#{elb_name}}",
|
132
|
+
{'Desired' => 2} # this is a Marker, a static line on the graph at y=2
|
133
|
+
),
|
134
|
+
dog.graphdef(
|
135
|
+
"ELB Latency",
|
136
|
+
[
|
137
|
+
"avg:aws.elb.latency{host:#{elb_name}}",
|
138
|
+
"max:aws.elb.latency{host:#{elb_name}}"
|
139
|
+
]
|
140
|
+
),
|
141
|
+
# Instance CPU Utilization from DataDog/EC2 integration
|
142
|
+
dog.graphdef(
|
143
|
+
"Instance EC2 CPU Utilization",
|
144
|
+
"avg:aws.ec2.cpuutilization{name:#{instance_name}}"
|
145
|
+
),
|
146
|
+
# Instance Free Memory from DataDog Agent on instance
|
147
|
+
dog.graphdef(
|
148
|
+
"Instance Free Memory",
|
149
|
+
"avg:system.mem.free{name:#{instance_name}}"
|
150
|
+
),
|
151
|
+
]
|
152
|
+
|
153
|
+
# upsert the TimeBoard
|
154
|
+
dog.upsert_timeboard("My Account-Unique Board Name", graphs)
|
155
|
+
```
|
156
|
+
|
157
|
+
## Development
|
158
|
+
|
159
|
+
1. ``bundle install --path vendor``
|
160
|
+
2. ``bundle exec rake pre_commit`` to ensure spec tests are passing and style is valid before making your changes
|
161
|
+
3. make your changes, and write spec tests for them. You can run ``bundle exec guard`` to continually run spec tests and rubocop when files change.
|
162
|
+
4. ``bundle exec rake pre_commit`` to confirm your tests pass and your style is valid. You should confirm 100% coverage. If you wish, you can run ``bundle exec guard`` to dynamically run rspec, rubocop and YARD when relevant files change.
|
163
|
+
5. Update ``ChangeLog.md`` for your changes.
|
164
|
+
6. Run ``bundle exec rake yard:serve`` to generate documentation for your Gem and serve it live at [http://localhost:8808](http://localhost:8808), and ensure it looks correct.
|
165
|
+
7. Open a pull request for your changes.
|
166
|
+
8. When shipped, merge the PR. CircleCI will test.
|
167
|
+
9. Deployment is done locally, with ``bundle exec rake release``.
|
168
|
+
|
169
|
+
When running inside CircleCI, rspec will place reports and artifacts under the right locations for CircleCI to archive them. When running outside of CircleCI, coverage reports will be written to ``coverage/`` and test reports (HTML and JUnit XML) will be written to ``results/``.
|
170
|
+
|
171
|
+
## License
|
172
|
+
|
173
|
+
The gem is available as open source under the terms of the
|
174
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'rake/clean'
|
6
|
+
require 'yard'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
require 'rubocop/rake_task'
|
9
|
+
|
10
|
+
CLOBBER.include 'pkg'
|
11
|
+
|
12
|
+
desc 'Run RuboCop on the lib directory'
|
13
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
14
|
+
task.fail_on_error = true
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
18
|
+
t.rspec_opts = ['--require', 'spec_helper']
|
19
|
+
t.pattern = 'spec/**/*_spec.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :yard do
|
23
|
+
YARD::Rake::YardocTask.new do |t|
|
24
|
+
t.name = 'generate'
|
25
|
+
t.files = ['lib/**/*.rb'] # optional
|
26
|
+
t.options = ['--private', '--protected'] # optional
|
27
|
+
t.stats_options = ['--list-undoc'] # optional
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'serve YARD documentation on port 8808 (restart to regenerate)'
|
31
|
+
task serve: [:generate] do
|
32
|
+
puts 'Running YARD server on port 8808'
|
33
|
+
puts 'Use Ctrl+C to exit server.'
|
34
|
+
YARD::CLI::Server.run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Run specs and rubocop before pushing'
|
39
|
+
task pre_commit: [:spec, :rubocop]
|
40
|
+
|
41
|
+
desc 'Display the list of available rake tasks'
|
42
|
+
task :help do
|
43
|
+
system('rake -T')
|
44
|
+
end
|
45
|
+
|
46
|
+
task default: [:help]
|
data/circle.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
dependencies:
|
3
|
+
cache_directories:
|
4
|
+
- "~/.rvm/gems"
|
5
|
+
pre:
|
6
|
+
- sudo add-apt-repository -y ppa:git-core/ppa && sudo apt-get update && sudo apt-get install git
|
7
|
+
override:
|
8
|
+
- 'bundle install'
|
9
|
+
- 'rvm 2.0.0-p598 exec bundle install'
|
10
|
+
- 'rvm 2.1.8 exec bundle install'
|
11
|
+
- 'rvm 2.2.5 exec bundle install'
|
12
|
+
- 'rvm 2.3.1 exec bundle install'
|
13
|
+
|
14
|
+
test:
|
15
|
+
override:
|
16
|
+
- 'bundle exec rake spec'
|
17
|
+
- 'bundle exec rake rubocop'
|
18
|
+
- 'rvm 2.0.0-p598 exec bundle exec rake spec'
|
19
|
+
- 'rvm 2.1.8 exec bundle exec rake spec'
|
20
|
+
- 'rvm 2.2.5 exec bundle exec rake spec'
|
21
|
+
- 'rvm 2.3.1 exec bundle exec rake spec'
|
22
|
+
- 'bundle exec rake yard:generate'
|
data/dogtrainer.gemspec
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/dogtrainer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['jantman']
|
6
|
+
gem.email = ['jason@jasonantman.com']
|
7
|
+
gem.summary = 'Wrapper around DataDog dogapi gem to simplify creation ' \
|
8
|
+
'and management of Monitors and Boards'
|
9
|
+
gem.description = [
|
10
|
+
'Provides a slightly opinionated wrapper class around DataDog\'s dogapi to',
|
11
|
+
' simplify the creation and updating of Monitors, TimeBoards and',
|
12
|
+
'ScreenBoards.'
|
13
|
+
].join(' ')
|
14
|
+
gem.homepage = 'http://github.com/Manheim/dogtrainer'
|
15
|
+
gem.license = 'MIT'
|
16
|
+
|
17
|
+
gem.add_runtime_dependency 'dogapi'
|
18
|
+
gem.add_runtime_dependency 'log4r', '>= 1.0'
|
19
|
+
|
20
|
+
# awful, but these are to allow use with ruby 2.1.x
|
21
|
+
gem.add_development_dependency 'ruby_dep', '1.3.1'
|
22
|
+
gem.add_development_dependency 'listen', '3.0.7'
|
23
|
+
|
24
|
+
# guard-yard uses Pry which needs readline. If we're in RVM, we'll need this:
|
25
|
+
gem.add_development_dependency 'rb-readline', '~> 0.5'
|
26
|
+
|
27
|
+
gem.add_development_dependency 'bundler'
|
28
|
+
gem.add_development_dependency 'cri', '~> 2'
|
29
|
+
gem.add_development_dependency 'diplomat', '~> 0.15'
|
30
|
+
gem.add_development_dependency 'faraday', '~> 0.9'
|
31
|
+
gem.add_development_dependency 'ghpages_deploy', '~> 1.3'
|
32
|
+
gem.add_development_dependency 'git', '~> 1.2', '>= 1.2.9.1'
|
33
|
+
gem.add_development_dependency 'guard', '~> 2.13'
|
34
|
+
gem.add_development_dependency 'guard-bundler', '~> 2.1'
|
35
|
+
gem.add_development_dependency 'guard-rspec', '~> 4.6.4'
|
36
|
+
gem.add_development_dependency 'guard-rubocop', '~> 1.2'
|
37
|
+
gem.add_development_dependency 'guard-yard', '~> 2.1'
|
38
|
+
gem.add_development_dependency 'json', '~> 1.8.3'
|
39
|
+
gem.add_development_dependency 'rake', '~> 10'
|
40
|
+
gem.add_development_dependency 'retries', '~> 0.0.5'
|
41
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
42
|
+
gem.add_development_dependency 'rspec_junit_formatter', '~> 0.2'
|
43
|
+
gem.add_development_dependency 'rubocop', '~> 0.37'
|
44
|
+
gem.add_development_dependency 'simplecov', '~> 0.11'
|
45
|
+
gem.add_development_dependency 'simplecov-console'
|
46
|
+
gem.add_development_dependency 'yard', '~> 0.8'
|
47
|
+
|
48
|
+
# ensure gem will only push to our Artifactory
|
49
|
+
# this requires rubygems >= 2.2.0
|
50
|
+
gem.metadata['allowed_push_host'] = 'https://rubygems.org'
|
51
|
+
|
52
|
+
gem.files = `git ls-files`.split($ORS)
|
53
|
+
.reject { |f| f =~ %r{^samples\/} }
|
54
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
55
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
56
|
+
gem.name = 'dogtrainer'
|
57
|
+
gem.require_paths = ['lib']
|
58
|
+
gem.version = DogTrainer::VERSION
|
59
|
+
end
|
data/lib/dogtrainer.rb
ADDED
@@ -0,0 +1,437 @@
|
|
1
|
+
require 'dogapi'
|
2
|
+
require 'dogapi/v1'
|
3
|
+
require 'dogtrainer/logging'
|
4
|
+
|
5
|
+
module DogTrainer
|
6
|
+
# Helper methods to upsert/ensure existence and configuration of DataDog
|
7
|
+
# Monitors, TimeBoards and ScreenBoards.
|
8
|
+
class API
|
9
|
+
include DogTrainer::Logging
|
10
|
+
|
11
|
+
# Initialize class; set instance configuration.
|
12
|
+
#
|
13
|
+
# @param api_key [String] DataDog API Key
|
14
|
+
# @param app_key [String] DataDog Application Key
|
15
|
+
# @param notify_to [String] DataDog notification recpipent string for
|
16
|
+
# monitors. This is generally one or more @-prefixed DataDog users or
|
17
|
+
# notification recipients. It can be set to nil if you are only managing
|
18
|
+
# screenboards and timeboards. For further information, see:
|
19
|
+
# http://docs.datadoghq.com/monitoring/#notifications
|
20
|
+
# @param repo_path [String] Git or HTTP URL to the repository containing
|
21
|
+
# code that calls this class. Will be added to notification messages so
|
22
|
+
# that humans know where to make changes to monitors. If nil, the return
|
23
|
+
# value of #get_repo_path
|
24
|
+
def initialize(api_key, app_key, notify_to, repo_path = nil)
|
25
|
+
logger.debug 'initializing DataDog API client'
|
26
|
+
@dog = Dogapi::Client.new(api_key, app_key)
|
27
|
+
@monitors = nil
|
28
|
+
@timeboards = nil
|
29
|
+
@screenboards = nil
|
30
|
+
@notify_to = notify_to
|
31
|
+
if repo_path.nil?
|
32
|
+
@repo_path = get_repo_path
|
33
|
+
logger.debug "using repo_path: #{@repo_path}"
|
34
|
+
else
|
35
|
+
@repo_path = repo_path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return a human-usable string identifying where to make changes to the
|
40
|
+
# resources created by this class. Returns the first of:
|
41
|
+
#
|
42
|
+
# 1. ``GIT_URL`` environment variable, if set and not empty
|
43
|
+
# 2. ``CIRCLE_REPOSITORY_URL`` environment variable, if set and not empty
|
44
|
+
# 3. If the code calling this class is part of a git repository on disk and
|
45
|
+
# ``git`` is present on the system and in PATH, the URL of the first
|
46
|
+
# remote for the repository.
|
47
|
+
#
|
48
|
+
# If none of these are found, an error will be raised.
|
49
|
+
def get_repo_path
|
50
|
+
%w(GIT_URL CIRCLE_REPOSITORY_URL).each do |vname|
|
51
|
+
return ENV[vname] if ENV.has_key?(vname) && !ENV[vname].empty?
|
52
|
+
end
|
53
|
+
# try to find git repository
|
54
|
+
# get the path to the calling code;
|
55
|
+
# caller[0] is #initialize, caller[1] is what instantiated the class
|
56
|
+
path, = caller[1].partition(':')
|
57
|
+
repo_path = get_git_url_for_directory(File.dirname(path))
|
58
|
+
if repo_path.nil?
|
59
|
+
raise 'Unable to determine source code path; please ' \
|
60
|
+
'specify repo_path option to DogTrainer::API'
|
61
|
+
end
|
62
|
+
repo_path
|
63
|
+
end
|
64
|
+
|
65
|
+
# Given the path to a directory on disk that may be a git repository,
|
66
|
+
# return the URL to its first remote, or nil otherwise.
|
67
|
+
#
|
68
|
+
# @param dir_path [String] Path to possible git repository
|
69
|
+
def get_git_url_for_directory(dir_path)
|
70
|
+
logger.debug "trying to find git remote for: #{dir_path}"
|
71
|
+
conf = nil
|
72
|
+
Dir.chdir(dir_path) do
|
73
|
+
begin
|
74
|
+
conf = `git config --local -l`
|
75
|
+
rescue
|
76
|
+
conf = nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return nil if conf.nil?
|
80
|
+
conf.split("\n").each do |line|
|
81
|
+
return Regexp.last_match(1) if line =~ /^remote\.[^\.]+\.url=(.+)/
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
#########################################
|
87
|
+
# BEGIN monitor-related shared methods. #
|
88
|
+
#########################################
|
89
|
+
|
90
|
+
# Given the name of a metric we're monitoring and the comparison method,
|
91
|
+
# generate alert messages for the monitor.
|
92
|
+
#
|
93
|
+
# This method is intended for internal use by the class, but can be
|
94
|
+
# overridden if the implementation is not desired.
|
95
|
+
#
|
96
|
+
# @param metric_desc [String] description/name of the metric being
|
97
|
+
# monitored.
|
98
|
+
# @param comparison [String] comparison operator or description for metric
|
99
|
+
# vs threshold; i.e. ">=", "<=", "=", "<", etc.
|
100
|
+
def generate_messages(metric_desc, comparison)
|
101
|
+
message = [
|
102
|
+
"{{#is_alert}}'#{metric_desc}' should be #{comparison} {{threshold}}, ",
|
103
|
+
"but is {{value}}.{{/is_alert}}\n",
|
104
|
+
"{{#is_recovery}}'#{metric_desc}' recovered (current value {{value}} ",
|
105
|
+
"is #{comparison} threshold of {{threshold}}).{{/is_recovery}}\n",
|
106
|
+
'(monitor and threshold configuration for this alert is managed by ',
|
107
|
+
"#{@repo_path}) #{@notify_to}"
|
108
|
+
].join('')
|
109
|
+
escalation = "'#{metric_desc}' is still in error state (current value " \
|
110
|
+
"{{value}} is #{comparison} threshold of {{threshold}})"
|
111
|
+
[message, escalation]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return a hash of parameters for a monitor with the specified
|
115
|
+
# configuration. For further information, see:
|
116
|
+
# http://docs.datadoghq.com/api/#monitors
|
117
|
+
#
|
118
|
+
# @param name [String] name for the monitor; must be unique per DataDog
|
119
|
+
# account
|
120
|
+
# @param message [String] alert/notification message for the monitor
|
121
|
+
# @param query [String] query for the monitor to evaluate
|
122
|
+
# @param threshold [Float] evaluation threshold for the monitor
|
123
|
+
# @param [Hash] options
|
124
|
+
# @option options [String] :escalation_message optional escalation message
|
125
|
+
# for escalation notifications. Defaults to nil.
|
126
|
+
# @option options [Boolean] :alert_no_data whether or not to alert on lack
|
127
|
+
# of data. Defaults to true.
|
128
|
+
# @option options [String] :mon_type type of monitor as defined in DataDog
|
129
|
+
# API docs. Defaults to 'metric alert'.
|
130
|
+
def params_for_monitor(
|
131
|
+
name,
|
132
|
+
message,
|
133
|
+
query,
|
134
|
+
threshold,
|
135
|
+
options = {
|
136
|
+
escalation_message: nil,
|
137
|
+
alert_no_data: true,
|
138
|
+
mon_type: 'metric alert'
|
139
|
+
}
|
140
|
+
)
|
141
|
+
options[:alert_no_data] = true unless options.key?(:alert_no_data)
|
142
|
+
options[:mon_type] = 'metric alert' unless options.key?(:mon_type)
|
143
|
+
monitor_data = {
|
144
|
+
'name' => name,
|
145
|
+
'type' => options[:mon_type],
|
146
|
+
'query' => query,
|
147
|
+
'message' => message,
|
148
|
+
'tags' => [],
|
149
|
+
'options' => {
|
150
|
+
'notify_audit' => false,
|
151
|
+
'locked' => false,
|
152
|
+
'timeout_h' => 0,
|
153
|
+
'silenced' => {},
|
154
|
+
'thresholds' => { 'critical' => threshold },
|
155
|
+
'require_full_window' => false,
|
156
|
+
'notify_no_data' => options[:alert_no_data],
|
157
|
+
'renotify_interval' => 60,
|
158
|
+
'no_data_timeframe' => 20
|
159
|
+
}
|
160
|
+
}
|
161
|
+
monitor_data['options']['escalation_message'] = \
|
162
|
+
options[:escalation_message] unless options[:escalation_message].nil?
|
163
|
+
monitor_data
|
164
|
+
end
|
165
|
+
|
166
|
+
# Create or update a monitor in DataDog with the given name and data/params.
|
167
|
+
# This method handles either creating the monitor if one with the same name
|
168
|
+
# doesn't already exist in the specified DataDog account, or else updating
|
169
|
+
# an existing monitor with the same name if one exists but the parameters
|
170
|
+
# differ.
|
171
|
+
#
|
172
|
+
# For further information on parameters and options, see:
|
173
|
+
# http://docs.datadoghq.com/api/#monitors
|
174
|
+
#
|
175
|
+
# This method calls #generate_messages to build the notification messages
|
176
|
+
# and #params_for_monitor to generate the parameters.
|
177
|
+
#
|
178
|
+
# @param mon_name [String] name for the monitor; must be unique per DataDog
|
179
|
+
# account
|
180
|
+
# @param query [String] query for the monitor to evaluate
|
181
|
+
# @param threshold [Float] evaluation threshold for the monitor
|
182
|
+
# @param comparator [String] comparison operator for metric vs threshold,
|
183
|
+
# describing the inverse of the query. I.e. if the query is checking for
|
184
|
+
# "< 100", then the comparator would be ">=".
|
185
|
+
# @param [Hash] options
|
186
|
+
# @option options [Boolean] :alert_no_data whether or not to alert on lack
|
187
|
+
# of data. Defaults to true.
|
188
|
+
# @option options [String] :mon_type type of monitor as defined in DataDog
|
189
|
+
# API docs. Defaults to 'metric alert'.
|
190
|
+
def upsert_monitor(
|
191
|
+
mon_name,
|
192
|
+
query,
|
193
|
+
threshold,
|
194
|
+
comparator,
|
195
|
+
options = { alert_no_data: true, mon_type: 'metric alert' }
|
196
|
+
)
|
197
|
+
options[:alert_no_data] = true unless options.key?(:alert_no_data)
|
198
|
+
options[:mon_type] = 'metric alert' unless options.key?(:mon_type)
|
199
|
+
message, escalation = generate_messages(mon_name, comparator)
|
200
|
+
mon_params = params_for_monitor(mon_name, message, query, threshold,
|
201
|
+
escalation_message: escalation,
|
202
|
+
alert_no_data: options[:alert_no_data],
|
203
|
+
mon_type: options[:mon_type])
|
204
|
+
logger.info "Upserting monitor: #{mon_name}"
|
205
|
+
monitor = get_existing_monitor_by_name(mon_name)
|
206
|
+
return create_monitor(mon_name, mon_params) if monitor.nil?
|
207
|
+
logger.debug "\tfound existing monitor id=#{monitor['id']}"
|
208
|
+
do_update = false
|
209
|
+
mon_params.each do |k, _v|
|
210
|
+
unless monitor.include?(k)
|
211
|
+
logger.debug "\tneeds update based on missing key: #{k}"
|
212
|
+
do_update = true
|
213
|
+
break
|
214
|
+
end
|
215
|
+
next unless monitor[k] != mon_params[k]
|
216
|
+
logger.debug "\tneeds update based on difference in key #{k}; " \
|
217
|
+
"current='#{monitor[k]}' desired='#{mon_params[k]}'"
|
218
|
+
do_update = true
|
219
|
+
break
|
220
|
+
end
|
221
|
+
unless do_update
|
222
|
+
logger.debug "\tmonitor is correct in DataDog."
|
223
|
+
return monitor['id']
|
224
|
+
end
|
225
|
+
res = @dog.update_monitor(monitor['id'], mon_params['query'], mon_params)
|
226
|
+
if res[0] == '200'
|
227
|
+
logger.info "\tMonitor #{monitor['id']} updated successfully"
|
228
|
+
return monitor['id']
|
229
|
+
else
|
230
|
+
logger.error "\tError updating monitor #{monitor['id']}: #{res}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Create a monitor that doesn't already exist; return its id
|
235
|
+
#
|
236
|
+
# @param mon_name [String] mane of the monitor to create
|
237
|
+
# @param mon_params [Hash] params to pass to the DataDog API call. Must
|
238
|
+
# include "type" and "query" keys.
|
239
|
+
def create_monitor(_mon_name, mon_params)
|
240
|
+
res = @dog.monitor(mon_params['type'], mon_params['query'], mon_params)
|
241
|
+
if res[0] == '200'
|
242
|
+
logger.info "\tMonitor #{res[1]['id']} created successfully"
|
243
|
+
return res[1]['id']
|
244
|
+
else
|
245
|
+
logger.error "\tError creating monitor: #{res}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Get all monitors from DataDog; return the one named ``mon_name`` or nil
|
250
|
+
#
|
251
|
+
# This caches all monitors from DataDog in an instance variable.
|
252
|
+
#
|
253
|
+
# @param mon_name [String] name of the monitor to return
|
254
|
+
def get_existing_monitor_by_name(mon_name)
|
255
|
+
if @monitors.nil?
|
256
|
+
@monitors = @dog.get_all_monitors(group_states: 'all')
|
257
|
+
logger.info "Found #{@monitors[1].length} existing monitors in DataDog"
|
258
|
+
if @monitors[1].empty?
|
259
|
+
logger.error 'ERROR: Docker API call returned no existing monitors.' \
|
260
|
+
' Something is wrong.'
|
261
|
+
exit 1
|
262
|
+
end
|
263
|
+
end
|
264
|
+
@monitors[1].each do |mon|
|
265
|
+
return mon if mon['name'] == mon_name
|
266
|
+
end
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
|
270
|
+
###########################################
|
271
|
+
# BEGIN dashboard-related shared methods. #
|
272
|
+
###########################################
|
273
|
+
|
274
|
+
# Create a graph definition (graphdef) to use with Boards APIs. For further
|
275
|
+
# information, see: http://docs.datadoghq.com/graphingjson/
|
276
|
+
#
|
277
|
+
# @param title [String] title of the graph
|
278
|
+
# @param queries [Array or String] a single string graph query, or an
|
279
|
+
# Array of graph query strings.
|
280
|
+
# @param markers [Hash] a hash of markers to set on the graph, in
|
281
|
+
# name => value format.
|
282
|
+
def graphdef(title, queries, markers = {})
|
283
|
+
queries = [queries] unless queries.is_a?(Array)
|
284
|
+
d = {
|
285
|
+
'definition' => {
|
286
|
+
'viz' => 'timeseries',
|
287
|
+
'requests' => []
|
288
|
+
},
|
289
|
+
'title' => title
|
290
|
+
}
|
291
|
+
queries.each do |q|
|
292
|
+
d['definition']['requests'] << {
|
293
|
+
'q' => q,
|
294
|
+
'conditional_formats' => [],
|
295
|
+
'type' => 'line'
|
296
|
+
}
|
297
|
+
end
|
298
|
+
unless markers.empty?
|
299
|
+
d['definition']['markers'] = []
|
300
|
+
markers.each do |name, val|
|
301
|
+
d['definition']['markers'] << {
|
302
|
+
'type' => 'error dashed',
|
303
|
+
'val' => val.to_s,
|
304
|
+
'value' => "y = #{val}",
|
305
|
+
'label' => "#{name}==#{val}"
|
306
|
+
}
|
307
|
+
end
|
308
|
+
end
|
309
|
+
d
|
310
|
+
end
|
311
|
+
|
312
|
+
# Create or update a timeboard in DataDog with the given name and
|
313
|
+
# data/params. For further information, see:
|
314
|
+
# http://docs.datadoghq.com/api/#timeboards
|
315
|
+
#
|
316
|
+
# @param dash_name [String] Account-unique dashboard name
|
317
|
+
# @param graphs [Array] Array of graphdefs to add to dashboard
|
318
|
+
def upsert_timeboard(dash_name, graphs)
|
319
|
+
logger.info "Upserting timeboard: #{dash_name}"
|
320
|
+
desc = "created by DogTrainer RubyGem via #{@repo_path}"
|
321
|
+
dash = get_existing_timeboard_by_name(dash_name)
|
322
|
+
if dash.nil?
|
323
|
+
d = @dog.create_dashboard(dash_name, desc, graphs)
|
324
|
+
logger.info "Created timeboard #{d[1]['dash']['id']}"
|
325
|
+
return
|
326
|
+
end
|
327
|
+
logger.debug "\tfound existing timeboard id=#{dash['dash']['id']}"
|
328
|
+
needs_update = false
|
329
|
+
if dash['dash']['description'] != desc
|
330
|
+
logger.debug "\tneeds update of description"
|
331
|
+
needs_update = true
|
332
|
+
end
|
333
|
+
if dash['dash']['title'] != dash_name
|
334
|
+
logger.debug "\tneeds update of title"
|
335
|
+
needs_update = true
|
336
|
+
end
|
337
|
+
if dash['dash']['graphs'] != graphs
|
338
|
+
logger.debug "\tneeds update of graphs"
|
339
|
+
needs_update = true
|
340
|
+
end
|
341
|
+
|
342
|
+
if needs_update
|
343
|
+
logger.info "\tUpdating timeboard #{dash['dash']['id']}"
|
344
|
+
@dog.update_dashboard(
|
345
|
+
dash['dash']['id'], dash_name, desc, graphs
|
346
|
+
)
|
347
|
+
logger.info "\tTimeboard updated."
|
348
|
+
else
|
349
|
+
logger.info "\tTimeboard is up-to-date"
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Create or update a screenboard in DataDog with the given name and
|
354
|
+
# data/params. For further information, see:
|
355
|
+
# http://docs.datadoghq.com/api/screenboards/ and
|
356
|
+
# http://docs.datadoghq.com/api/?lang=ruby#screenboards
|
357
|
+
#
|
358
|
+
# @param dash_name [String] Account-unique dashboard name
|
359
|
+
# @param widgets [Array] Array of Hash widget definitions to pass to
|
360
|
+
# the DataDog API. For further information, see:
|
361
|
+
# http://docs.datadoghq.com/api/screenboards/
|
362
|
+
def upsert_screenboard(dash_name, widgets)
|
363
|
+
logger.info "Upserting screenboard: #{dash_name}"
|
364
|
+
desc = "created by DogTrainer RubyGem via #{@repo_path}"
|
365
|
+
dash = get_existing_screenboard_by_name(dash_name)
|
366
|
+
if dash.nil?
|
367
|
+
d = @dog.create_screenboard(board_title: dash_name,
|
368
|
+
description: desc,
|
369
|
+
widgets: widgets)
|
370
|
+
logger.info "Created screenboard #{d[1]['id']}"
|
371
|
+
return
|
372
|
+
end
|
373
|
+
logger.debug "\tfound existing screenboard id=#{dash['id']}"
|
374
|
+
needs_update = false
|
375
|
+
if dash['description'] != desc
|
376
|
+
logger.debug "\tneeds update of description"
|
377
|
+
needs_update = true
|
378
|
+
end
|
379
|
+
if dash['board_title'] != dash_name
|
380
|
+
logger.debug "\tneeds update of title"
|
381
|
+
needs_update = true
|
382
|
+
end
|
383
|
+
if dash['widgets'] != widgets
|
384
|
+
logger.debug "\tneeds update of widgets"
|
385
|
+
needs_update = true
|
386
|
+
end
|
387
|
+
|
388
|
+
if needs_update
|
389
|
+
logger.info "\tUpdating screenboard #{dash['id']}"
|
390
|
+
@dog.update_screenboard(dash['id'], board_title: dash_name,
|
391
|
+
description: desc,
|
392
|
+
widgets: widgets)
|
393
|
+
logger.info "\tScreenboard updated."
|
394
|
+
else
|
395
|
+
logger.info "\tScreenboard is up-to-date"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# get all timeboards from DataDog; return the one named ``dash_name`` or nil
|
400
|
+
# returns the timeboard definition hash from the DataDog API
|
401
|
+
def get_existing_timeboard_by_name(dash_name)
|
402
|
+
if @timeboards.nil?
|
403
|
+
@timeboards = @dog.get_dashboards
|
404
|
+
puts "Found #{@timeboards[1]['dashes'].length} existing timeboards " \
|
405
|
+
'in DataDog'
|
406
|
+
if @timeboards[1]['dashes'].empty?
|
407
|
+
puts 'ERROR: Docker API call returned no existing timeboards. ' \
|
408
|
+
'Something is wrong.'
|
409
|
+
exit 1
|
410
|
+
end
|
411
|
+
end
|
412
|
+
@timeboards[1]['dashes'].each do |dash|
|
413
|
+
return @dog.get_dashboard(dash['id'])[1] if dash['title'] == dash_name
|
414
|
+
end
|
415
|
+
nil
|
416
|
+
end
|
417
|
+
|
418
|
+
# get all screenboards from DataDog; return the one named ``dash_name`` or
|
419
|
+
# nil returns the screenboard definition hash from the DataDog API
|
420
|
+
def get_existing_screenboard_by_name(dash_name)
|
421
|
+
if @screenboards.nil?
|
422
|
+
@screenboards = @dog.get_all_screenboards
|
423
|
+
puts "Found #{@screenboards[1]['screenboards'].length} existing " \
|
424
|
+
'screenboards in DataDog'
|
425
|
+
if @screenboards[1]['screenboards'].empty?
|
426
|
+
puts 'ERROR: Docker API call returned no existing screenboards. ' \
|
427
|
+
'Something is wrong.'
|
428
|
+
exit 1
|
429
|
+
end
|
430
|
+
end
|
431
|
+
@screenboards[1]['screenboards'].each do |dash|
|
432
|
+
return @dog.get_screenboard(dash['id'])[1] if dash['title'] == dash_name
|
433
|
+
end
|
434
|
+
nil
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|