doggy 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: d45de1eb568f088f9c702bbb55e2961691337c45
4
+ data.tar.gz: 733512dea50824d0800967443ab6827e79d155b7
5
+ SHA512:
6
+ metadata.gz: 60049f997c3552d938fbf8d70e709f525716939658e3f08a2d4101b679fd12b82285d5266355f6349bc8ba6d998d803f56a2c6dbbaed8987fe90bf64f0a6ba21
7
+ data.tar.gz: ab72d81d8e5d83561e67c126ad52e0a5320f28ccc4905943e60d529e5d2c7617b862e8ffc677a7e38f42e093e9a2983df385914468a39273ba4e789fa75bb0f6
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in doggy.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Vlad Gorodetsky
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,61 @@
1
+ # Doggy
2
+
3
+ Doggy manages your DataDog dashboards, alerts, monitors, and screenboards.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'doggy'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install doggy
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ # Export your DataDog credentials or use ejson
25
+ $ export DATADOG_API_KEY=api_key_goes_here
26
+ $ export DATADOG_APP_KEY=app_key_goes_here
27
+
28
+ # Download selected items from DataDog
29
+ $ doggy pull ID ID
30
+
31
+ # Download all items
32
+ $ doggy pull
33
+
34
+ # Upload selected items to DataDog
35
+ $ doggy push ID ID ID
36
+
37
+ # Upload all items to DataDog
38
+ $ doggy push
39
+
40
+ # Create a new dashboard
41
+ $ doggy create dash 'My New Dash'
42
+
43
+ # Delete selected items from both DataDog and local storage
44
+ $ doggy delete ID ID ID
45
+ ```
46
+
47
+ Note that we currently don't support global upload due to high risk of overwriting things. We'll turn this feature on after initial testing period.
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bundle install` to install dependencies.
52
+
53
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
54
+
55
+ ## Contributing
56
+
57
+ 1. Fork it ( https://github.com/bai/doggy/fork )
58
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
59
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
60
+ 4. Push to the branch (`git push origin my-new-feature`)
61
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/doggy ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Exit cleanly from an early interrupt
4
+ Signal.trap('INT') { exit 1 }
5
+
6
+ require 'bundler/setup'
7
+ require 'doggy/cli'
8
+
9
+ Doggy::CLI.start(ARGV, :debug => true)
data/doggy.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'doggy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "doggy"
8
+ spec.version = Doggy::VERSION
9
+ spec.authors = ["Vlad Gorodetsky"]
10
+ spec.email = ["v@gor.io"]
11
+
12
+ spec.summary = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.}
13
+ spec.description = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.}
14
+ spec.homepage = "http://github.com/bai/doggy"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.9"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_dependency "thor", "~> 0.19"
25
+ spec.add_dependency "dogapi", "~> 1.17"
26
+ spec.add_dependency "thread", "~> 0.2"
27
+ spec.add_dependency "ejson", "~> 1.0"
28
+ end
data/lib/doggy.rb ADDED
@@ -0,0 +1,118 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'dogapi'
6
+
7
+ require 'doggy/version'
8
+ require 'doggy/client'
9
+ require 'doggy/worker'
10
+ require 'doggy/serializer/json'
11
+ require 'doggy/serializer/yaml'
12
+ require 'doggy/model/dash'
13
+ require 'doggy/model/monitor'
14
+ require 'doggy/model/screen'
15
+
16
+ module Doggy
17
+ DOG_SKIP_REGEX = /\[dog\s+skip\]/i.freeze
18
+ DEFAULT_SERIALIZER_CLASS = Doggy::Serializer::Json
19
+
20
+ class DoggyError < StandardError
21
+ def self.status_code(code)
22
+ define_method(:status_code) { code }
23
+ end
24
+ end
25
+
26
+ class InvalidOption < DoggyError; status_code(15); end
27
+ class InvalidItemType < DoggyError; status_code(10); end
28
+
29
+ class << self
30
+ # @option arguments [Constant] :serializer A specific serializer class to use, will be initialized by doggy and passed the object instance
31
+ def serializer(options = {})
32
+ @serializer ||= options[:serializer] ? options[:serializer] : DEFAULT_SERIALIZER_CLASS
33
+ end
34
+
35
+ def client
36
+ Doggy::Client.new
37
+ end
38
+
39
+ # Absolute path of where alerts are stored on the filesystem.
40
+ #
41
+ # @return [Pathname]
42
+ def alerts_path
43
+ @alerts_path ||= Pathname.new('alerts').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) }
44
+ end
45
+
46
+ # Absolute path of where dashes are stored on the filesystem.
47
+ #
48
+ # @return [Pathname]
49
+ def dashes_path
50
+ @dashes_path ||= Pathname.new('dashes').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) }
51
+ end
52
+
53
+ # Absolute path of where screens are stored on the filesystem.
54
+ #
55
+ # @return [Pathname]
56
+ def screens_path
57
+ @screens_path ||= Pathname.new('screens').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) }
58
+ end
59
+
60
+ # Cleans up directory
61
+ def clean_dir(dir)
62
+ Dir.foreach(dir) { |f| fn = File.join(dir, f); File.delete(fn) if f != '.' && f != '..'}
63
+ end
64
+
65
+ def all_local_items
66
+ @all_local_items ||= Dir[Doggy.dashes_path.join('**/*'), Doggy.alerts_path.join('**/*'), Doggy.screens_path.join('**/*')].inject({}) { |memo, file| memo.merge load_item(f) }
67
+ end
68
+
69
+ def load_item(f)
70
+ filetype = File.extname(f)
71
+
72
+ item = case filetype
73
+ when '.yaml', '.yml' then Doggy::Serializer::Yaml.load(File.read(f))
74
+ when '.json' then Doggy::Serializer::Json.load(File.read(f))
75
+ else raise InvalidItemType
76
+ end
77
+
78
+ { [ determine_type(item), item['id'] ] => item }
79
+ end
80
+
81
+ def determine_type(item)
82
+ return 'dash' if item['graphs']
83
+ return 'monitor' if item['message']
84
+ return 'screen' if item['board_title']
85
+ raise InvalidItemType
86
+ end
87
+
88
+ def emit_shipit_deployment
89
+ Doggy.client.dog.emit_event(
90
+ Dogapi::Event.new(ENV['REVISION'], msg_title: "ShipIt Deployment by #{ENV['USER']}", tags: %w(audit shipit), source_type_name: 'shipit')
91
+ )
92
+ rescue => e
93
+ puts "Exception: #{e.message}"
94
+ end
95
+
96
+ def current_version
97
+ now = Time.now.to_i
98
+ month_ago = now - 3600 * 24 * 30
99
+ events = Doggy.client.dog.stream(month_ago, now, tags: %w(audit shipit))[1]['events']
100
+
101
+ events[0]['text'] # most recetly deployed SHA
102
+ rescue => e
103
+ puts "Exception: #{e.message}"
104
+ end
105
+
106
+ def all_remote_dashes
107
+ @all_remote_dashes ||= Doggy.client.dog.get_dashboards[1]['dashes'].inject({}) do |memo, dash|
108
+ memo.merge([ 'dash', dash['id'] ] => dash)
109
+ end
110
+ end
111
+
112
+ def all_remote_monitors
113
+ @all_remote_monitors ||= Doggy.client.dog.get_all_monitors[1].inject({}) do |memo, monitor|
114
+ memo.merge([ 'monitor', monitor['id'] ] => monitor)
115
+ end
116
+ end
117
+ end
118
+ end
data/lib/doggy/cli.rb ADDED
@@ -0,0 +1,97 @@
1
+ require 'thor'
2
+ require 'doggy'
3
+
4
+ module Doggy
5
+ class CLI < Thor
6
+ include Thor::Actions
7
+
8
+ def self.start(*)
9
+ super
10
+ rescue Exception => e
11
+ raise e
12
+ ensure
13
+ end
14
+
15
+ def initialize(*args)
16
+ super
17
+ rescue UnknownArgumentError => e
18
+ raise Doggy::InvalidOption, e.message
19
+ ensure
20
+ self.options ||= {}
21
+ end
22
+
23
+ check_unknown_options!(:except => [:config, :exec])
24
+ stop_on_unknown_option! :exec
25
+
26
+ desc "pull [SPACE SEPARATED IDs]", "Pulls objects from DataDog"
27
+ long_desc <<-D
28
+ Pull objects from DataDog. If pull is successful, Doggy exits with a status of 0.
29
+ If not, the error is displayed and Doggy exits status 1.
30
+ D
31
+ def pull(*ids)
32
+ require 'doggy/cli/pull'
33
+ Pull.new(options.dup, ids).run
34
+ end
35
+
36
+ desc "push [SPACE SEPARATED IDs]", "Pushes objects to DataDog"
37
+ long_desc <<-D
38
+ Pushes objects to DataDog. If push is successful, Doggy exits with a status of 0.
39
+ If not, the error is displayed and Doggy exits status 1.
40
+ D
41
+ def push(*ids)
42
+ require 'doggy/cli/push'
43
+ Push.new(options.dup, ids).run
44
+ end
45
+
46
+ desc "create OBJECT_TYPE OBJECT_NAME", "Creates a new object on DataDog"
47
+ long_desc <<-D
48
+ Creates a new object on DataDog. If create is successful, Doggy exits with a status of 0.
49
+ If not, the error is displayed and Doggy exits status 1.
50
+ D
51
+ def create(kind, name)
52
+ require 'doggy/cli/create'
53
+ Create.new(options.dup, kind, name).run
54
+ end
55
+
56
+ desc "delete SPACE SEPARATED IDs", "Deletes objects from DataDog"
57
+ long_desc <<-D
58
+ Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0.
59
+ If not, the error is displayed and Doggy exits status 1.
60
+ D
61
+ def delete(*ids)
62
+ require 'doggy/cli/delete'
63
+ Delete.new(options.dup, ids).run
64
+ end
65
+
66
+ desc "mute [SPACE SEPARATED IDs]", "Mutes monitor on DataDog"
67
+ long_desc <<-D
68
+ Mutes monitor on DataDog. If mute is successful, Doggy exits with a status of 0.
69
+ If not, the error is displayed and Doggy exits status 1.
70
+ D
71
+ def mute(*ids)
72
+ require 'doggy/cli/mute'
73
+ Mute.new(options.dup, ids).run
74
+ end
75
+
76
+ desc "unmute [SPACE SEPARATED IDs]", "Unmutes monitor on DataDog"
77
+ long_desc <<-D
78
+ Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0.
79
+ If not, the error is displayed and Doggy exits status 1.
80
+ D
81
+ def unmute(*ids)
82
+ require 'doggy/cli/unmute'
83
+ Unmute.new(options.dup, ids).run
84
+ end
85
+
86
+ desc "version", "Detects the most recent SHA deployed by ShipIt"
87
+ long_desc <<-D
88
+ Scans DataDog event stream for shipit events what contain most recently deployed version
89
+ of DataDog properties.
90
+ If not, the error is displayed and Doggy exits status 1.
91
+ D
92
+ def version
93
+ require 'doggy/cli/version'
94
+ Version.new.run
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,25 @@
1
+ module Doggy
2
+ class CLI::Create
3
+ attr_reader :options, :kind, :name
4
+
5
+ def initialize(options, kind, name)
6
+ @options = options
7
+ @kind = kind
8
+ @name = name
9
+ end
10
+
11
+ def run
12
+ begin
13
+ case kind
14
+ when 'dash', 'dashboard' then Doggy::Dash.create(name)
15
+ when 'alert', 'monitor' then Doggy::Monitor.create(name)
16
+ when 'screen', 'screenboard' then Doggy::Screen.create(name)
17
+ else puts 'Unknown item type'
18
+ end
19
+ rescue DoggyError
20
+ puts "Create failed."
21
+ exit 1
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Doggy
2
+ class CLI::Delete
3
+ attr_reader :options, :ids
4
+
5
+ def initialize(options, ids)
6
+ @options = options
7
+ @ids = ids
8
+ end
9
+
10
+ def run
11
+ begin
12
+ Doggy::Dash.delete(ids)
13
+ Doggy::Monitor.delete(ids)
14
+ Doggy::Screen.delete(ids)
15
+ rescue DoggyError
16
+ puts "Create failed."
17
+ exit 1
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Doggy
2
+ class CLI::Mute
3
+ attr_reader :options, :ids
4
+
5
+ def initialize(options, ids)
6
+ @options = options
7
+ @ids = ids
8
+ end
9
+
10
+ def run
11
+ begin
12
+ Doggy::Monitor.mute(ids)
13
+ rescue DoggyError
14
+ puts "Mute failed."
15
+ exit 1
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module Doggy
2
+ class CLI::Pull
3
+ attr_reader :options, :ids
4
+
5
+ def initialize(options, ids)
6
+ @options = options
7
+ @ids = ids
8
+ end
9
+
10
+ def run
11
+ begin
12
+ if ids.any?
13
+ Doggy::Dash.download(ids)
14
+ Doggy::Monitor.download(ids)
15
+ Doggy::Screen.download(ids)
16
+ else
17
+ Doggy::Dash.download_all
18
+ Doggy::Monitor.download_all
19
+ Doggy::Screen.download_all
20
+ end
21
+ rescue DoggyError
22
+ puts "Pull failed."
23
+ exit 1
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module Doggy
2
+ class CLI::Push
3
+ attr_reader :options, :ids
4
+
5
+ def initialize(options, ids)
6
+ @options = options
7
+ @ids = ids
8
+ end
9
+
10
+ def run
11
+ begin
12
+ if ids.any?
13
+ Doggy::Dash.upload(ids)
14
+ Doggy::Monitor.upload(ids)
15
+ Doggy::Screen.upload(ids)
16
+ else
17
+ Doggy::Dash.upload_all
18
+ Doggy::Monitor.upload_all
19
+ Doggy::Screen.upload_all
20
+ Doggy.emit_shipit_deployment if ENV['SHIPIT']
21
+ end
22
+ rescue DoggyError
23
+ puts "Push failed."
24
+ exit 1
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ module Doggy
2
+ class CLI::Unmute
3
+ attr_reader :options, :ids
4
+
5
+ def initialize(options, ids)
6
+ @options = options
7
+ @ids = ids
8
+ end
9
+
10
+ def run
11
+ begin
12
+ Doggy::Monitor.unmute(ids)
13
+ rescue DoggyError
14
+ puts "Unmute failed."
15
+ exit 1
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Doggy
2
+ class CLI::Version
3
+ def run
4
+ begin
5
+ print Doggy.current_version
6
+ rescue DoggyError
7
+ puts "Could not fetch latest SHA from DataDog."
8
+ exit 1
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ class Dogapi::APIService
2
+ attr_reader :api_key, :application_key # as they are useless in the parent class
3
+ end
4
+
5
+ module Doggy
6
+ class Client
7
+ def api_key
8
+ @api_key ||= ENV.fetch('DATADOG_API_KEY', ejson_config[:datadog_api_key])
9
+ rescue => e
10
+ puts "[DogSync#api_key] Exception: #{e.message}"
11
+ raise
12
+ end
13
+
14
+ def app_key
15
+ @app_key ||= ENV.fetch('DATADOG_APP_KEY', ejson_config[:datadog_app_key])
16
+ rescue => e
17
+ puts "[DogSync#app_key] Exception: #{e.message}"
18
+ raise
19
+ end
20
+
21
+ def dog
22
+ @dog ||= Dogapi::Client.new(api_key, app_key)
23
+ end
24
+
25
+ def api_service
26
+ @api_service ||= Dogapi::APIService.new(api_key, app_key)
27
+ end
28
+
29
+ def api_service_params
30
+ @api_service_params ||= { api_key: Doggy.client.api_service.api_key, application_key: Doggy.client.api_service.application_key }
31
+ end
32
+
33
+ private
34
+
35
+ def ejson_config
36
+ @ejson_config ||= begin
37
+ if File.exists?('secrets.json')
38
+ secrets = JSON.parse(File.read('secrets.json'))
39
+ { datadog_api_key: secrets['datadog_api_key'], datadog_app_key: secrets['datadog_app_key'] }
40
+ else
41
+ {}
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,91 @@
1
+ module Doggy
2
+ class Dash
3
+ def initialize(**options)
4
+ @id = options[:id]
5
+ @title = options[:title] || raw_local['title']
6
+ @description = options[:description] || raw_local['description']
7
+ @graphs = options[:graphs] || raw_local['graphs']
8
+ @template_variables = options[:template_variables] || raw_local['template_variables']
9
+ end
10
+
11
+ def self.download_all
12
+ ids = Doggy.client.dog.get_dashboards[1]['dashes'].map { |d| d['id'] }
13
+ puts "Downloading #{ids.size} dashboards..."
14
+ Doggy.clean_dir(Doggy.dashes_path)
15
+ download(ids)
16
+ rescue => e
17
+ puts "Exception: #{e.message}"
18
+ end
19
+
20
+ def self.upload_all
21
+ ids = Dir[Doggy.dashes_path.join('*')].map { |f| File.basename(f, '.*') }
22
+ puts "Uploading #{ids.size} dashboards from #{Doggy.dashes_path}: #{ids.join(', ')}"
23
+ upload(ids)
24
+ rescue => e
25
+ puts "Exception: #{e.message}"
26
+ end
27
+
28
+ def self.download(ids)
29
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids])
30
+ end
31
+
32
+ def self.upload(ids)
33
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids])
34
+ end
35
+
36
+ def self.create(name)
37
+ # This graphs placeholder is required as you cannot create an empty dashboard via API
38
+ dash = new(title: name, description: '', graphs: [{
39
+ "definition" => {
40
+ "events" => [],
41
+ "requests" => [
42
+ {"q" => "avg:system.mem.free{*}"}
43
+ ],
44
+ "viz" => "timeseries"
45
+ },
46
+ "title" => "Average Memory Free"
47
+ }])
48
+ dash.push
49
+ dash.save
50
+ end
51
+
52
+ def raw
53
+ @raw ||= Doggy.client.dog.get_dashboard(@id)[1]['dash'].sort.to_h
54
+ end
55
+
56
+ def raw_local
57
+ return {} unless File.exists?(path)
58
+ @raw_local ||= Doggy.serializer.load(File.read(path))
59
+ end
60
+
61
+ def save
62
+ puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist
63
+ return if raw['title'] =~ Doggy::DOG_SKIP_REGEX
64
+ File.write(path, Doggy.serializer.dump(raw))
65
+ end
66
+
67
+ def push
68
+ return unless File.exists?(path)
69
+ return if @title =~ Doggy::DOG_SKIP_REGEX
70
+ if @id
71
+ Doggy.client.dog.update_dashboard(@id, @title, @description, @graphs, @template_variables)
72
+ else
73
+ dash = Doggy.client.dog.create_dashboard(@title, @description, @graphs)
74
+ # FIXME: Remove duplication
75
+ @id = dash[1]['id']
76
+ @graphs = dash[1]['graphs']
77
+ end
78
+ end
79
+
80
+ def delete
81
+ Doggy.client.dog.delete_dashboard(@id)
82
+ File.unlink(path)
83
+ end
84
+
85
+ private
86
+
87
+ def path
88
+ "#{Doggy.dashes_path}/#{@id}.json"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,129 @@
1
+ module Doggy
2
+ class Monitor
3
+ def initialize(**options)
4
+ @id = options[:id]
5
+ @query = options[:query]
6
+ @silenced = options[:silenced]
7
+ @name = options[:name]
8
+ @timeout_h = options[:timeout_h]
9
+ @message = options[:message]
10
+ @notify_audit = options[:notify_audit]
11
+ @notify_no_data = options[:notify_no_data]
12
+ @renotify_interval = options[:renotify_interval]
13
+ @escalation_message = options[:escalation_message]
14
+ @no_data_timeframe = options[:no_data_timeframe]
15
+ @silenced_timeout_ts = options[:silenced_timeout_ts]
16
+ end
17
+
18
+ def self.download_all
19
+ ids = Doggy.client.dog.get_all_alerts[1]['alerts'].map { |d| d['id'] }
20
+ puts "Downloading #{ids.size} alerts..."
21
+ Doggy.clean_dir(Doggy.alerts_path)
22
+ download(ids)
23
+ rescue => e
24
+ puts "Exception: #{e.message}"
25
+ end
26
+
27
+ def self.upload_all
28
+ ids = Dir[Doggy.alerts_path.join('*')].map { |f| File.basename(f, '.*') }
29
+ puts "Uploading #{ids.size} alerts from #{Doggy.alerts_path}: #{ids.join(', ')}"
30
+ upload(ids)
31
+ rescue => e
32
+ puts "Exception: #{e.message}"
33
+ end
34
+
35
+ def self.download(ids)
36
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids])
37
+ end
38
+
39
+ def self.upload(ids)
40
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids])
41
+ end
42
+
43
+ def self.mute(ids)
44
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).mute }.call([*ids])
45
+ end
46
+
47
+ def self.unmute(ids)
48
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).unmute }.call([*ids])
49
+ end
50
+
51
+ def self.create(name)
52
+ # Adding a placeholder query as it's a mandatory parameter
53
+ item = new(name: name, query: 'avg(last_1m):avg:system.load.1{*} > 100')
54
+ item.push
55
+ item.save
56
+ end
57
+
58
+ def raw
59
+ @raw ||= begin
60
+ alert = Doggy.client.dog.get_monitor(@id)[1]
61
+ alert.delete('state')
62
+ alert.delete('overall_state')
63
+ alert['options'].delete('silenced')
64
+ alert.sort.to_h
65
+ end
66
+ end
67
+
68
+ def raw_local
69
+ return unless File.exists?(path)
70
+ @raw_local ||= Doggy.serializer.load(File.read(path))
71
+ end
72
+
73
+ def save
74
+ puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist
75
+ return if raw['name'] =~ Doggy::DOG_SKIP_REGEX
76
+ File.write(path, Doggy.serializer.dump(raw))
77
+ end
78
+
79
+ def mute
80
+ Doggy.client.dog.mute_monitor(@id)
81
+ end
82
+
83
+ def unmute
84
+ Doggy.client.dog.unmute_monitor(@id)
85
+ end
86
+
87
+ def push
88
+ return if @name =~ Doggy::DOG_SKIP_REGEX
89
+ if @id
90
+ return unless File.exists?(path)
91
+
92
+ Doggy.client.dog.update_monitor(@id, @query || raw_local['query'], {
93
+ name: @name || raw_local['name'],
94
+ timeout_h: @timeout_h || raw_local['timeout_h'],
95
+ message: @message || raw_local['message'],
96
+ notify_audit: @notify_audit || raw_local['notify_audit'],
97
+ notify_no_data: @notify_no_data || raw_local['notify_no_data'],
98
+ renotify_interval: @renotify_interval || raw_local['renotify_interval'],
99
+ escalation_message: @escalation_message || raw_local['escalation_message'],
100
+ no_data_timeframe: @no_data_timeframe || raw_local['no_data_timeframe'],
101
+ silenced_timeout_ts: @silenced_timeout_ts || raw_local['silenced_timeout_ts'],
102
+ options: {
103
+ silenced: mute_state_for(@id),
104
+ },
105
+ })
106
+ else
107
+ result = Doggy.client.dog.monitor('metric alert', @query, name: @name)
108
+ @id = result[1]['id']
109
+ end
110
+ end
111
+
112
+ def delete
113
+ Doggy.client.dog.delete_alert(@id)
114
+ File.unlink(path)
115
+ end
116
+
117
+ private
118
+
119
+ def path
120
+ "#{Doggy.alerts_path}/#{@id}.json"
121
+ end
122
+
123
+ def mute_state_for(id)
124
+ if remote_state = Doggy.all_remote_monitors.detect { |key, value| key == [ 'monitor', id.to_i ] }
125
+ remote_state[1]['options']['silenced'] if remote_state[1]['options']
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,76 @@
1
+ module Doggy
2
+ class Screen
3
+ def initialize(**options)
4
+ @id = options[:id]
5
+ @description = options[:description] || raw_local
6
+ end
7
+
8
+ def self.download_all
9
+ ids = Doggy.client.dog.get_all_screenboards[1]['screenboards'].map { |d| d['id'] }
10
+ puts "Downloading #{ids.size} screenboards..."
11
+ Doggy.clean_dir(Doggy.screens_path)
12
+ download(ids)
13
+ rescue => e
14
+ puts "Exception: #{e.message}"
15
+ end
16
+
17
+ def self.upload_all
18
+ ids = Dir[Doggy.screens_path.join('*')].map { |f| File.basename(f, '.*') }
19
+ puts "Uploading #{ids.size} screenboards from #{Doggy.screens_path}: #{ids.join(', ')}"
20
+ upload(ids)
21
+ rescue => e
22
+ puts "Exception: #{e.message}"
23
+ end
24
+
25
+ def self.download(ids)
26
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids])
27
+ end
28
+
29
+ def self.upload(ids)
30
+ Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids])
31
+ end
32
+
33
+ def self.create(name)
34
+ item = new(description: { 'board_title' => name, 'widgets' => [] })
35
+ item.push
36
+ item.save
37
+ end
38
+
39
+ def raw
40
+ @raw ||= Doggy.client.dog.get_screenboard(@id)[1].sort.to_h
41
+ end
42
+
43
+ def raw_local
44
+ return {} unless File.exists?(path)
45
+ @raw_local ||= Doggy.serializer.load(File.read(path))
46
+ end
47
+
48
+ def save
49
+ puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist
50
+ return if raw['board_title'] =~ Doggy::DOG_SKIP_REGEX
51
+ File.write(path, Doggy.serializer.dump(raw))
52
+ end
53
+
54
+ def push
55
+ return if @description =~ Doggy::DOG_SKIP_REGEX
56
+ if @id
57
+ Doggy.client.dog.update_screenboard(@id, @description)
58
+ else
59
+ result = Doggy.client.dog.create_screenboard(@description)
60
+ @id = result[1]['id']
61
+ @description = result[1]
62
+ end
63
+ end
64
+
65
+ def delete
66
+ Doggy.client.dog.delete_screenboard(@id)
67
+ File.unlink(path)
68
+ end
69
+
70
+ private
71
+
72
+ def path
73
+ "#{Doggy.screens_path}/#{@id}.json"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,15 @@
1
+ module Doggy
2
+ module Serializer
3
+ class Json
4
+ # De-serialize a Hash from JSON string
5
+ def self.load(string)
6
+ ::JSON.load(string)
7
+ end
8
+
9
+ # Serialize a Hash to JSON string
10
+ def self.dump(object, options = {})
11
+ ::JSON.pretty_generate(object, options) + "\n"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Doggy
2
+ module Serializer
3
+ class Yaml
4
+ # De-serialize a Hash from YAML string
5
+ def self.load(string)
6
+ ::YAML.load(string)
7
+ end
8
+
9
+ # Serialize a Hash to YAML string
10
+ def self.dump(object, options = {})
11
+ ::YAML.dump(object, options)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Doggy
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,31 @@
1
+ require 'thread'
2
+ require 'thread/pool'
3
+
4
+ Thread.abort_on_exception = true
5
+
6
+ module Doggy
7
+ class Worker
8
+ # Spawn 10 threads for HTTP requests.
9
+ CONCURRENT_STREAMS = 10
10
+
11
+ def initialize(options = {}, &runner)
12
+ @runner = runner
13
+ @threads = options.fetch(:threads)
14
+ end
15
+
16
+ def call(jobs)
17
+ results = []
18
+ pool = Thread::Pool.new(@threads)
19
+ tasks = jobs.map { |job|
20
+ pool.process {
21
+ results << [ job, @runner.call(job) ]
22
+ }
23
+ }
24
+ pool.shutdown
25
+ if task_with_errors = tasks.detect { |task| task.exception }
26
+ raise task_with_errors.exception
27
+ end
28
+ results
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: doggy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vlad Gorodetsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.19'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.19'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dogapi
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.17'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.17'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thread
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ejson
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ description: Syncs DataDog dashboards, alerts, screenboards, and monitors.
98
+ email:
99
+ - v@gor.io
100
+ executables:
101
+ - doggy
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - Gemfile
107
+ - LICENSE.txt
108
+ - README.md
109
+ - Rakefile
110
+ - bin/doggy
111
+ - doggy.gemspec
112
+ - lib/doggy.rb
113
+ - lib/doggy/cli.rb
114
+ - lib/doggy/cli/create.rb
115
+ - lib/doggy/cli/delete.rb
116
+ - lib/doggy/cli/mute.rb
117
+ - lib/doggy/cli/pull.rb
118
+ - lib/doggy/cli/push.rb
119
+ - lib/doggy/cli/unmute.rb
120
+ - lib/doggy/cli/version.rb
121
+ - lib/doggy/client.rb
122
+ - lib/doggy/model/dash.rb
123
+ - lib/doggy/model/monitor.rb
124
+ - lib/doggy/model/screen.rb
125
+ - lib/doggy/serializer/json.rb
126
+ - lib/doggy/serializer/yaml.rb
127
+ - lib/doggy/version.rb
128
+ - lib/doggy/worker.rb
129
+ homepage: http://github.com/bai/doggy
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.4.8
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Syncs DataDog dashboards, alerts, screenboards, and monitors.
153
+ test_files: []