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