doggy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/Rakefile +1 -0
- data/bin/doggy +9 -0
- data/doggy.gemspec +28 -0
- data/lib/doggy.rb +118 -0
- data/lib/doggy/cli.rb +97 -0
- data/lib/doggy/cli/create.rb +25 -0
- data/lib/doggy/cli/delete.rb +21 -0
- data/lib/doggy/cli/mute.rb +19 -0
- data/lib/doggy/cli/pull.rb +27 -0
- data/lib/doggy/cli/push.rb +28 -0
- data/lib/doggy/cli/unmute.rb +19 -0
- data/lib/doggy/cli/version.rb +12 -0
- data/lib/doggy/client.rb +46 -0
- data/lib/doggy/model/dash.rb +91 -0
- data/lib/doggy/model/monitor.rb +129 -0
- data/lib/doggy/model/screen.rb +76 -0
- data/lib/doggy/serializer/json.rb +15 -0
- data/lib/doggy/serializer/yaml.rb +15 -0
- data/lib/doggy/version.rb +3 -0
- data/lib/doggy/worker.rb +31 -0
- metadata +153 -0
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
data/Gemfile
ADDED
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
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
|
data/lib/doggy/client.rb
ADDED
@@ -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
|
data/lib/doggy/worker.rb
ADDED
@@ -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: []
|