diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cb6eeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5ffe34e --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in doggy.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c47c178 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Vlad Gorodetsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa685f8 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Doggy + +Doggy manages your DataDog dashboards, alerts, monitors, and screenboards. + +## Installation + +Add this line to your Gemfile: + +```ruby +gem 'doggy' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install doggy + +## Usage + +``` +# Export your DataDog credentials or use ejson +$ export DATADOG_API_KEY=api_key_goes_here +$ export DATADOG_APP_KEY=app_key_goes_here + +# Download selected items from DataDog +$ doggy pull ID ID + +# Download all items +$ doggy pull + +# Upload selected items to DataDog +$ doggy push ID ID ID + +# Upload all items to DataDog +$ doggy push + +# Create a new dashboard +$ doggy create dash 'My New Dash' + +# Delete selected items from both DataDog and local storage +$ doggy delete ID ID ID +``` + +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. + +## Development + +After checking out the repo, run `bundle install` to install dependencies. + +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). + +## Contributing + +1. Fork it ( https://github.com/bai/doggy/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/bin/doggy b/bin/doggy new file mode 100755 index 0000000..715eba6 --- /dev/null +++ b/bin/doggy @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +# Exit cleanly from an early interrupt +Signal.trap('INT') { exit 1 } + +require 'bundler/setup' +require 'doggy/cli' + +Doggy::CLI.start(ARGV, :debug => true) diff --git a/doggy.gemspec b/doggy.gemspec new file mode 100644 index 0000000..5377669 --- /dev/null +++ b/doggy.gemspec @@ -0,0 +1,28 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'doggy/version' + +Gem::Specification.new do |spec| + spec.name = "doggy" + spec.version = Doggy::VERSION + spec.authors = ["Vlad Gorodetsky"] + spec.email = ["v@gor.io"] + + spec.summary = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.} + spec.description = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.} + spec.homepage = "http://github.com/bai/doggy" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.bindir = "bin" + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.9" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_dependency "thor", "~> 0.19" + spec.add_dependency "dogapi", "~> 1.17" + spec.add_dependency "thread", "~> 0.2" + spec.add_dependency "ejson", "~> 1.0" +end diff --git a/lib/doggy.rb b/lib/doggy.rb new file mode 100644 index 0000000..5ff4df4 --- /dev/null +++ b/lib/doggy.rb @@ -0,0 +1,118 @@ +require 'fileutils' +require 'pathname' +require 'json' +require 'yaml' +require 'dogapi' + +require 'doggy/version' +require 'doggy/client' +require 'doggy/worker' +require 'doggy/serializer/json' +require 'doggy/serializer/yaml' +require 'doggy/model/dash' +require 'doggy/model/monitor' +require 'doggy/model/screen' + +module Doggy + DOG_SKIP_REGEX = /\[dog\s+skip\]/i.freeze + DEFAULT_SERIALIZER_CLASS = Doggy::Serializer::Json + + class DoggyError < StandardError + def self.status_code(code) + define_method(:status_code) { code } + end + end + + class InvalidOption < DoggyError; status_code(15); end + class InvalidItemType < DoggyError; status_code(10); end + + class << self + # @option arguments [Constant] :serializer A specific serializer class to use, will be initialized by doggy and passed the object instance + def serializer(options = {}) + @serializer ||= options[:serializer] ? options[:serializer] : DEFAULT_SERIALIZER_CLASS + end + + def client + Doggy::Client.new + end + + # Absolute path of where alerts are stored on the filesystem. + # + # @return [Pathname] + def alerts_path + @alerts_path ||= Pathname.new('alerts').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) } + end + + # Absolute path of where dashes are stored on the filesystem. + # + # @return [Pathname] + def dashes_path + @dashes_path ||= Pathname.new('dashes').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) } + end + + # Absolute path of where screens are stored on the filesystem. + # + # @return [Pathname] + def screens_path + @screens_path ||= Pathname.new('screens').expand_path(Dir.pwd).expand_path.tap { |path| FileUtils.mkdir_p(path) } + end + + # Cleans up directory + def clean_dir(dir) + Dir.foreach(dir) { |f| fn = File.join(dir, f); File.delete(fn) if f != '.' && f != '..'} + end + + def all_local_items + @all_local_items ||= Dir[Doggy.dashes_path.join('**/*'), Doggy.alerts_path.join('**/*'), Doggy.screens_path.join('**/*')].inject({}) { |memo, file| memo.merge load_item(f) } + end + + def load_item(f) + filetype = File.extname(f) + + item = case filetype + when '.yaml', '.yml' then Doggy::Serializer::Yaml.load(File.read(f)) + when '.json' then Doggy::Serializer::Json.load(File.read(f)) + else raise InvalidItemType + end + + { [ determine_type(item), item['id'] ] => item } + end + + def determine_type(item) + return 'dash' if item['graphs'] + return 'monitor' if item['message'] + return 'screen' if item['board_title'] + raise InvalidItemType + end + + def emit_shipit_deployment + Doggy.client.dog.emit_event( + Dogapi::Event.new(ENV['REVISION'], msg_title: "ShipIt Deployment by #{ENV['USER']}", tags: %w(audit shipit), source_type_name: 'shipit') + ) + rescue => e + puts "Exception: #{e.message}" + end + + def current_version + now = Time.now.to_i + month_ago = now - 3600 * 24 * 30 + events = Doggy.client.dog.stream(month_ago, now, tags: %w(audit shipit))[1]['events'] + + events[0]['text'] # most recetly deployed SHA + rescue => e + puts "Exception: #{e.message}" + end + + def all_remote_dashes + @all_remote_dashes ||= Doggy.client.dog.get_dashboards[1]['dashes'].inject({}) do |memo, dash| + memo.merge([ 'dash', dash['id'] ] => dash) + end + end + + def all_remote_monitors + @all_remote_monitors ||= Doggy.client.dog.get_all_monitors[1].inject({}) do |memo, monitor| + memo.merge([ 'monitor', monitor['id'] ] => monitor) + end + end + end +end diff --git a/lib/doggy/cli.rb b/lib/doggy/cli.rb new file mode 100644 index 0000000..07fbc15 --- /dev/null +++ b/lib/doggy/cli.rb @@ -0,0 +1,97 @@ +require 'thor' +require 'doggy' + +module Doggy + class CLI < Thor + include Thor::Actions + + def self.start(*) + super + rescue Exception => e + raise e + ensure + end + + def initialize(*args) + super + rescue UnknownArgumentError => e + raise Doggy::InvalidOption, e.message + ensure + self.options ||= {} + end + + check_unknown_options!(:except => [:config, :exec]) + stop_on_unknown_option! :exec + + desc "pull [SPACE SEPARATED IDs]", "Pulls objects from DataDog" + long_desc <<-D + Pull objects from DataDog. If pull is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def pull(*ids) + require 'doggy/cli/pull' + Pull.new(options.dup, ids).run + end + + desc "push [SPACE SEPARATED IDs]", "Pushes objects to DataDog" + long_desc <<-D + Pushes objects to DataDog. If push is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def push(*ids) + require 'doggy/cli/push' + Push.new(options.dup, ids).run + end + + desc "create OBJECT_TYPE OBJECT_NAME", "Creates a new object on DataDog" + long_desc <<-D + Creates a new object on DataDog. If create is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def create(kind, name) + require 'doggy/cli/create' + Create.new(options.dup, kind, name).run + end + + desc "delete SPACE SEPARATED IDs", "Deletes objects from DataDog" + long_desc <<-D + Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def delete(*ids) + require 'doggy/cli/delete' + Delete.new(options.dup, ids).run + end + + desc "mute [SPACE SEPARATED IDs]", "Mutes monitor on DataDog" + long_desc <<-D + Mutes monitor on DataDog. If mute is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def mute(*ids) + require 'doggy/cli/mute' + Mute.new(options.dup, ids).run + end + + desc "unmute [SPACE SEPARATED IDs]", "Unmutes monitor on DataDog" + long_desc <<-D + Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0. + If not, the error is displayed and Doggy exits status 1. + D + def unmute(*ids) + require 'doggy/cli/unmute' + Unmute.new(options.dup, ids).run + end + + desc "version", "Detects the most recent SHA deployed by ShipIt" + long_desc <<-D + Scans DataDog event stream for shipit events what contain most recently deployed version + of DataDog properties. + If not, the error is displayed and Doggy exits status 1. + D + def version + require 'doggy/cli/version' + Version.new.run + end + end +end diff --git a/lib/doggy/cli/create.rb b/lib/doggy/cli/create.rb new file mode 100644 index 0000000..1ebe74c --- /dev/null +++ b/lib/doggy/cli/create.rb @@ -0,0 +1,25 @@ +module Doggy + class CLI::Create + attr_reader :options, :kind, :name + + def initialize(options, kind, name) + @options = options + @kind = kind + @name = name + end + + def run + begin + case kind + when 'dash', 'dashboard' then Doggy::Dash.create(name) + when 'alert', 'monitor' then Doggy::Monitor.create(name) + when 'screen', 'screenboard' then Doggy::Screen.create(name) + else puts 'Unknown item type' + end + rescue DoggyError + puts "Create failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/delete.rb b/lib/doggy/cli/delete.rb new file mode 100644 index 0000000..0057b49 --- /dev/null +++ b/lib/doggy/cli/delete.rb @@ -0,0 +1,21 @@ +module Doggy + class CLI::Delete + attr_reader :options, :ids + + def initialize(options, ids) + @options = options + @ids = ids + end + + def run + begin + Doggy::Dash.delete(ids) + Doggy::Monitor.delete(ids) + Doggy::Screen.delete(ids) + rescue DoggyError + puts "Create failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/mute.rb b/lib/doggy/cli/mute.rb new file mode 100644 index 0000000..0d47f89 --- /dev/null +++ b/lib/doggy/cli/mute.rb @@ -0,0 +1,19 @@ +module Doggy + class CLI::Mute + attr_reader :options, :ids + + def initialize(options, ids) + @options = options + @ids = ids + end + + def run + begin + Doggy::Monitor.mute(ids) + rescue DoggyError + puts "Mute failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/pull.rb b/lib/doggy/cli/pull.rb new file mode 100644 index 0000000..b96b30d --- /dev/null +++ b/lib/doggy/cli/pull.rb @@ -0,0 +1,27 @@ +module Doggy + class CLI::Pull + attr_reader :options, :ids + + def initialize(options, ids) + @options = options + @ids = ids + end + + def run + begin + if ids.any? + Doggy::Dash.download(ids) + Doggy::Monitor.download(ids) + Doggy::Screen.download(ids) + else + Doggy::Dash.download_all + Doggy::Monitor.download_all + Doggy::Screen.download_all + end + rescue DoggyError + puts "Pull failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/push.rb b/lib/doggy/cli/push.rb new file mode 100644 index 0000000..bcb9f1c --- /dev/null +++ b/lib/doggy/cli/push.rb @@ -0,0 +1,28 @@ +module Doggy + class CLI::Push + attr_reader :options, :ids + + def initialize(options, ids) + @options = options + @ids = ids + end + + def run + begin + if ids.any? + Doggy::Dash.upload(ids) + Doggy::Monitor.upload(ids) + Doggy::Screen.upload(ids) + else + Doggy::Dash.upload_all + Doggy::Monitor.upload_all + Doggy::Screen.upload_all + Doggy.emit_shipit_deployment if ENV['SHIPIT'] + end + rescue DoggyError + puts "Push failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/unmute.rb b/lib/doggy/cli/unmute.rb new file mode 100644 index 0000000..db2ce4d --- /dev/null +++ b/lib/doggy/cli/unmute.rb @@ -0,0 +1,19 @@ +module Doggy + class CLI::Unmute + attr_reader :options, :ids + + def initialize(options, ids) + @options = options + @ids = ids + end + + def run + begin + Doggy::Monitor.unmute(ids) + rescue DoggyError + puts "Unmute failed." + exit 1 + end + end + end +end diff --git a/lib/doggy/cli/version.rb b/lib/doggy/cli/version.rb new file mode 100644 index 0000000..7d234ae --- /dev/null +++ b/lib/doggy/cli/version.rb @@ -0,0 +1,12 @@ +module Doggy + class CLI::Version + def run + begin + print Doggy.current_version + rescue DoggyError + puts "Could not fetch latest SHA from DataDog." + exit 1 + end + end + end +end diff --git a/lib/doggy/client.rb b/lib/doggy/client.rb new file mode 100644 index 0000000..d02f795 --- /dev/null +++ b/lib/doggy/client.rb @@ -0,0 +1,46 @@ +class Dogapi::APIService + attr_reader :api_key, :application_key # as they are useless in the parent class +end + +module Doggy + class Client + def api_key + @api_key ||= ENV.fetch('DATADOG_API_KEY', ejson_config[:datadog_api_key]) + rescue => e + puts "[DogSync#api_key] Exception: #{e.message}" + raise + end + + def app_key + @app_key ||= ENV.fetch('DATADOG_APP_KEY', ejson_config[:datadog_app_key]) + rescue => e + puts "[DogSync#app_key] Exception: #{e.message}" + raise + end + + def dog + @dog ||= Dogapi::Client.new(api_key, app_key) + end + + def api_service + @api_service ||= Dogapi::APIService.new(api_key, app_key) + end + + def api_service_params + @api_service_params ||= { api_key: Doggy.client.api_service.api_key, application_key: Doggy.client.api_service.application_key } + end + + private + + def ejson_config + @ejson_config ||= begin + if File.exists?('secrets.json') + secrets = JSON.parse(File.read('secrets.json')) + { datadog_api_key: secrets['datadog_api_key'], datadog_app_key: secrets['datadog_app_key'] } + else + {} + end + end + end + end +end diff --git a/lib/doggy/model/dash.rb b/lib/doggy/model/dash.rb new file mode 100644 index 0000000..e53906f --- /dev/null +++ b/lib/doggy/model/dash.rb @@ -0,0 +1,91 @@ +module Doggy + class Dash + def initialize(**options) + @id = options[:id] + @title = options[:title] || raw_local['title'] + @description = options[:description] || raw_local['description'] + @graphs = options[:graphs] || raw_local['graphs'] + @template_variables = options[:template_variables] || raw_local['template_variables'] + end + + def self.download_all + ids = Doggy.client.dog.get_dashboards[1]['dashes'].map { |d| d['id'] } + puts "Downloading #{ids.size} dashboards..." + Doggy.clean_dir(Doggy.dashes_path) + download(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.upload_all + ids = Dir[Doggy.dashes_path.join('*')].map { |f| File.basename(f, '.*') } + puts "Uploading #{ids.size} dashboards from #{Doggy.dashes_path}: #{ids.join(', ')}" + upload(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.download(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) + end + + def self.upload(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) + end + + def self.create(name) + # This graphs placeholder is required as you cannot create an empty dashboard via API + dash = new(title: name, description: '', graphs: [{ + "definition" => { + "events" => [], + "requests" => [ + {"q" => "avg:system.mem.free{*}"} + ], + "viz" => "timeseries" + }, + "title" => "Average Memory Free" + }]) + dash.push + dash.save + end + + def raw + @raw ||= Doggy.client.dog.get_dashboard(@id)[1]['dash'].sort.to_h + end + + def raw_local + return {} unless File.exists?(path) + @raw_local ||= Doggy.serializer.load(File.read(path)) + end + + def save + puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist + return if raw['title'] =~ Doggy::DOG_SKIP_REGEX + File.write(path, Doggy.serializer.dump(raw)) + end + + def push + return unless File.exists?(path) + return if @title =~ Doggy::DOG_SKIP_REGEX + if @id + Doggy.client.dog.update_dashboard(@id, @title, @description, @graphs, @template_variables) + else + dash = Doggy.client.dog.create_dashboard(@title, @description, @graphs) + # FIXME: Remove duplication + @id = dash[1]['id'] + @graphs = dash[1]['graphs'] + end + end + + def delete + Doggy.client.dog.delete_dashboard(@id) + File.unlink(path) + end + + private + + def path + "#{Doggy.dashes_path}/#{@id}.json" + end + end +end diff --git a/lib/doggy/model/monitor.rb b/lib/doggy/model/monitor.rb new file mode 100644 index 0000000..2008768 --- /dev/null +++ b/lib/doggy/model/monitor.rb @@ -0,0 +1,129 @@ +module Doggy + class Monitor + def initialize(**options) + @id = options[:id] + @query = options[:query] + @silenced = options[:silenced] + @name = options[:name] + @timeout_h = options[:timeout_h] + @message = options[:message] + @notify_audit = options[:notify_audit] + @notify_no_data = options[:notify_no_data] + @renotify_interval = options[:renotify_interval] + @escalation_message = options[:escalation_message] + @no_data_timeframe = options[:no_data_timeframe] + @silenced_timeout_ts = options[:silenced_timeout_ts] + end + + def self.download_all + ids = Doggy.client.dog.get_all_alerts[1]['alerts'].map { |d| d['id'] } + puts "Downloading #{ids.size} alerts..." + Doggy.clean_dir(Doggy.alerts_path) + download(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.upload_all + ids = Dir[Doggy.alerts_path.join('*')].map { |f| File.basename(f, '.*') } + puts "Uploading #{ids.size} alerts from #{Doggy.alerts_path}: #{ids.join(', ')}" + upload(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.download(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) + end + + def self.upload(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) + end + + def self.mute(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).mute }.call([*ids]) + end + + def self.unmute(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).unmute }.call([*ids]) + end + + def self.create(name) + # Adding a placeholder query as it's a mandatory parameter + item = new(name: name, query: 'avg(last_1m):avg:system.load.1{*} > 100') + item.push + item.save + end + + def raw + @raw ||= begin + alert = Doggy.client.dog.get_monitor(@id)[1] + alert.delete('state') + alert.delete('overall_state') + alert['options'].delete('silenced') + alert.sort.to_h + end + end + + def raw_local + return unless File.exists?(path) + @raw_local ||= Doggy.serializer.load(File.read(path)) + end + + def save + puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist + return if raw['name'] =~ Doggy::DOG_SKIP_REGEX + File.write(path, Doggy.serializer.dump(raw)) + end + + def mute + Doggy.client.dog.mute_monitor(@id) + end + + def unmute + Doggy.client.dog.unmute_monitor(@id) + end + + def push + return if @name =~ Doggy::DOG_SKIP_REGEX + if @id + return unless File.exists?(path) + + Doggy.client.dog.update_monitor(@id, @query || raw_local['query'], { + name: @name || raw_local['name'], + timeout_h: @timeout_h || raw_local['timeout_h'], + message: @message || raw_local['message'], + notify_audit: @notify_audit || raw_local['notify_audit'], + notify_no_data: @notify_no_data || raw_local['notify_no_data'], + renotify_interval: @renotify_interval || raw_local['renotify_interval'], + escalation_message: @escalation_message || raw_local['escalation_message'], + no_data_timeframe: @no_data_timeframe || raw_local['no_data_timeframe'], + silenced_timeout_ts: @silenced_timeout_ts || raw_local['silenced_timeout_ts'], + options: { + silenced: mute_state_for(@id), + }, + }) + else + result = Doggy.client.dog.monitor('metric alert', @query, name: @name) + @id = result[1]['id'] + end + end + + def delete + Doggy.client.dog.delete_alert(@id) + File.unlink(path) + end + + private + + def path + "#{Doggy.alerts_path}/#{@id}.json" + end + + def mute_state_for(id) + if remote_state = Doggy.all_remote_monitors.detect { |key, value| key == [ 'monitor', id.to_i ] } + remote_state[1]['options']['silenced'] if remote_state[1]['options'] + end + end + end +end diff --git a/lib/doggy/model/screen.rb b/lib/doggy/model/screen.rb new file mode 100644 index 0000000..d7677ca --- /dev/null +++ b/lib/doggy/model/screen.rb @@ -0,0 +1,76 @@ +module Doggy + class Screen + def initialize(**options) + @id = options[:id] + @description = options[:description] || raw_local + end + + def self.download_all + ids = Doggy.client.dog.get_all_screenboards[1]['screenboards'].map { |d| d['id'] } + puts "Downloading #{ids.size} screenboards..." + Doggy.clean_dir(Doggy.screens_path) + download(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.upload_all + ids = Dir[Doggy.screens_path.join('*')].map { |f| File.basename(f, '.*') } + puts "Uploading #{ids.size} screenboards from #{Doggy.screens_path}: #{ids.join(', ')}" + upload(ids) + rescue => e + puts "Exception: #{e.message}" + end + + def self.download(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) + end + + def self.upload(ids) + Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) + end + + def self.create(name) + item = new(description: { 'board_title' => name, 'widgets' => [] }) + item.push + item.save + end + + def raw + @raw ||= Doggy.client.dog.get_screenboard(@id)[1].sort.to_h + end + + def raw_local + return {} unless File.exists?(path) + @raw_local ||= Doggy.serializer.load(File.read(path)) + end + + def save + puts raw['errors'] and return if raw['errors'] # do now download an item if it doesn't exist + return if raw['board_title'] =~ Doggy::DOG_SKIP_REGEX + File.write(path, Doggy.serializer.dump(raw)) + end + + def push + return if @description =~ Doggy::DOG_SKIP_REGEX + if @id + Doggy.client.dog.update_screenboard(@id, @description) + else + result = Doggy.client.dog.create_screenboard(@description) + @id = result[1]['id'] + @description = result[1] + end + end + + def delete + Doggy.client.dog.delete_screenboard(@id) + File.unlink(path) + end + + private + + def path + "#{Doggy.screens_path}/#{@id}.json" + end + end +end diff --git a/lib/doggy/serializer/json.rb b/lib/doggy/serializer/json.rb new file mode 100644 index 0000000..2f07fda --- /dev/null +++ b/lib/doggy/serializer/json.rb @@ -0,0 +1,15 @@ +module Doggy + module Serializer + class Json + # De-serialize a Hash from JSON string + def self.load(string) + ::JSON.load(string) + end + + # Serialize a Hash to JSON string + def self.dump(object, options = {}) + ::JSON.pretty_generate(object, options) + "\n" + end + end + end +end diff --git a/lib/doggy/serializer/yaml.rb b/lib/doggy/serializer/yaml.rb new file mode 100644 index 0000000..bf0258f --- /dev/null +++ b/lib/doggy/serializer/yaml.rb @@ -0,0 +1,15 @@ +module Doggy + module Serializer + class Yaml + # De-serialize a Hash from YAML string + def self.load(string) + ::YAML.load(string) + end + + # Serialize a Hash to YAML string + def self.dump(object, options = {}) + ::YAML.dump(object, options) + end + end + end +end diff --git a/lib/doggy/version.rb b/lib/doggy/version.rb new file mode 100644 index 0000000..dcd13ba --- /dev/null +++ b/lib/doggy/version.rb @@ -0,0 +1,3 @@ +module Doggy + VERSION = '0.1.0' +end diff --git a/lib/doggy/worker.rb b/lib/doggy/worker.rb new file mode 100644 index 0000000..4ed292f --- /dev/null +++ b/lib/doggy/worker.rb @@ -0,0 +1,31 @@ +require 'thread' +require 'thread/pool' + +Thread.abort_on_exception = true + +module Doggy + class Worker + # Spawn 10 threads for HTTP requests. + CONCURRENT_STREAMS = 10 + + def initialize(options = {}, &runner) + @runner = runner + @threads = options.fetch(:threads) + end + + def call(jobs) + results = [] + pool = Thread::Pool.new(@threads) + tasks = jobs.map { |job| + pool.process { + results << [ job, @runner.call(job) ] + } + } + pool.shutdown + if task_with_errors = tasks.detect { |task| task.exception } + raise task_with_errors.exception + end + results + end + end +end