dogtrainer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ Version 0.0.1
2
+
3
+ - initial release
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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'
@@ -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,6 @@
1
+ # define top level of module in the right file
2
+ module DogTrainer
3
+ end
4
+
5
+ gem_libs_dir = "#{File.dirname File.absolute_path(__FILE__)}/dogtrainer"
6
+ Dir.glob("#{gem_libs_dir}/*.rb") { |file| require file }
@@ -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