dogtrainer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: [![CircleCI](https://circleci.com/gh/manheim/dogtrainer.svg?style=svg)](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
|