From 6b23d7ffa239065e75287bebe23bdc3e324d6e57 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Mon, 6 Jan 2014 17:10:35 -0500 Subject: [PATCH 01/10] isolate version-specific capistrano code more cleanly --- lib/capistrano/datadog.rb | 102 +++++++---------------------------- lib/capistrano/datadog/v2.rb | 81 ++++++++++++++++++++++++++++ lib/capistrano/datadog/v3.rb | 2 + 3 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 lib/capistrano/datadog/v2.rb create mode 100644 lib/capistrano/datadog/v3.rb diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index fd4ce7c8..66558029 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -1,50 +1,7 @@ -require "benchmark" require "etc" require "digest/md5" -require "socket" -require "time" -require "timeout" -require "delegate" - require "dogapi" -# Monkeypatch capistrano to collect data about the tasks that it's running -module Capistrano - class Configuration - module Execution - # Attempts to locate the task at the given fully-qualified path, and - # execute it. If no such task exists, a Capistrano::NoSuchTaskError - # will be raised. - # Also, capture the time the task took to execute, and the logs it - # outputted for submission to Datadog - def find_and_execute_task(path, hooks = {}) - task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" - result = nil - reporter = Capistrano::Datadog.reporter - timing = Benchmark.measure(task.fully_qualified_name) do - # Set the current task so that the logger knows which task to - # associate the logs with - reporter.current_task = task.fully_qualified_name - trigger(hooks[:before], task) if hooks[:before] - result = execute_task(task) - trigger(hooks[:after], task) if hooks[:after] - reporter.current_task = nil - end - # Collect the timing in a list for later reporting - reporter.record_task task, timing - result - end - end - end - - class Logger - # Make the device attribute writeable so we can swap it out - # with something that captures logging out by task - attr_accessor :device - end -end - - module Capistrano module Datadog # Singleton method for Reporter @@ -52,6 +9,17 @@ def self.reporter() @reporter || @reporter = Reporter.new end + def self.cap_version() + if @cap_version.nil? then + if Configuration.respond_to? :instance then + @cap_version = :v2 + else + @cap_version = :v3 + end + end + @cap_version + end + # Collects info about the tasks that ran in order to submit to Datadog class Reporter attr_accessor :current_task @@ -111,46 +79,14 @@ def report() end end - class LogCapture < SimpleDelegator - def puts(message) - Capistrano::Datadog::reporter.record_log message - __getobj__.puts message - end - end - end - - if Configuration.respond_to? :instance then - # Capistrano v2 - Configuration.instance(:must_exist).load do - # Wrap the existing logging target with the Datadog capture class - logger.device = Datadog::LogCapture.new logger.device - - # Trigger the Datadog submission once all the tasks have run - on :exit, "datadog:submit" - namespace :datadog do - desc "Submit the tasks that have run to Datadog as events" - task :submit do |ns| - begin - api_key = variables[:datadog_api_key] - if api_key - dog = Dogapi::Client.new(api_key) - Datadog::reporter.report.each do |event| - dog.emit_event event - end - else - puts "No api key set, not submitting to Datadog" - end - rescue Timeout::Error => e - puts "Could not submit to Datadog, request timed out." - rescue => e - puts "Could not submit to Datadog: #{e.inspect}\n#{e.backtrace.join("\n")}" - end - end - end - end - else - # No support yet for Capistrano v3 - puts "No Datadog events will be sent since Datadog currently only supports Capistrano v2. For more info, see https://github.com/DataDog/dogapi-rb/issues/40." end +end +case Capistrano::Datadog::cap_version +when :v2 + require 'capistrano/datadog/v2' +when :v3 + require 'capistrano/datadog/v3' +else + puts "Unknown version: {Capistrano::Datadog::cap_version.inspect}" end diff --git a/lib/capistrano/datadog/v2.rb b/lib/capistrano/datadog/v2.rb new file mode 100644 index 00000000..3654a508 --- /dev/null +++ b/lib/capistrano/datadog/v2.rb @@ -0,0 +1,81 @@ +require "benchmark" +require "delegate" + +# Capistrano v2 + +# Monkeypatch capistrano to collect data about the tasks that it's running +module Capistrano + class Configuration + module Execution + # Attempts to locate the task at the given fully-qualified path, and + # execute it. If no such task exists, a Capistrano::NoSuchTaskError + # will be raised. + # Also, capture the time the task took to execute, and the logs it + # outputted for submission to Datadog + def find_and_execute_task(path, hooks = {}) + task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" + result = nil + reporter = Capistrano::Datadog.reporter + timing = Benchmark.measure(task.fully_qualified_name) do + # Set the current task so that the logger knows which task to + # associate the logs with + reporter.current_task = task.fully_qualified_name + trigger(hooks[:before], task) if hooks[:before] + result = execute_task(task) + trigger(hooks[:after], task) if hooks[:after] + reporter.current_task = nil + end + # Collect the timing in a list for later reporting + reporter.record_task task, timing + result + end + end + end + + class Logger + # Make the device attribute writeable so we can swap it out + # with something that captures logging out by task + attr_accessor :device + end +end + + +module Capistrano + module Datadog + class LogCapture < SimpleDelegator + def puts(message) + Capistrano::Datadog::reporter.record_log message + __getobj__.puts message + end + end + + Configuration.instance(:must_exist).load do + # Wrap the existing logging target with the Datadog capture class + logger.device = Datadog::LogCapture.new logger.device + + # Trigger the Datadog submission once all the tasks have run + on :exit, "datadog:submit" + namespace :datadog do + desc "Submit the tasks that have run to Datadog as events" + task :submit do |ns| + begin + api_key = variables[:datadog_api_key] + if api_key + dog = Dogapi::Client.new(api_key) + Datadog::reporter.report.each do |event| + dog.emit_event event + end + else + puts "No api key set, not submitting to Datadog" + end + rescue Timeout::Error => e + puts "Could not submit to Datadog, request timed out." + rescue => e + puts "Could not submit to Datadog: #{e.inspect}\n#{e.backtrace.join("\n")}" + end + end + end + end + end +end + diff --git a/lib/capistrano/datadog/v3.rb b/lib/capistrano/datadog/v3.rb new file mode 100644 index 00000000..25758a9c --- /dev/null +++ b/lib/capistrano/datadog/v3.rb @@ -0,0 +1,2 @@ +# No support yet for Capistrano v3 +puts "No Datadog events will be sent since Datadog currently only supports Capistrano v2. For more info, see https://github.com/DataDog/dogapi-rb/issues/40." From ca6749b57004621bcd943c4d470a9950c52b24e7 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Mon, 6 Jan 2014 18:15:24 -0500 Subject: [PATCH 02/10] First stab at capistrano v3 support. Doesn't get log output yet --- lib/capistrano/datadog.rb | 29 ++++++++++++++++++++++------- lib/capistrano/datadog/v2.rb | 32 +++++++++++++------------------- lib/capistrano/datadog/v3.rb | 26 ++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index 66558029..26546065 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -1,5 +1,7 @@ require "etc" require "digest/md5" +require "timeout" + require "dogapi" module Capistrano @@ -20,6 +22,23 @@ def self.cap_version() @cap_version end + def self.submit(api_key) + begin + if api_key + dog = Dogapi::Client.new(api_key) + reporter.report.each do |event| + dog.emit_event event + end + else + puts "No api key set, not submitting to Datadog" + end + rescue Timeout::Error => e + puts "Could not submit to Datadog, request timed out." + rescue => e + puts "Could not submit to Datadog: #{e.inspect}\n#{e.backtrace.join("\n")}" + end + end + # Collects info about the tasks that ran in order to submit to Datadog class Reporter attr_accessor :current_task @@ -30,14 +49,10 @@ def initialize() @logging_output = {} end - def record_task(task, timing) - roles = task.options[:roles] - if roles.is_a? Proc - roles = roles.call - end + def record_task(task_name, timing, roles) @tasks << { - :name => task.fully_qualified_name, - :timing => timing.real, + :name => task_name, + :timing => timing, :roles => roles } end diff --git a/lib/capistrano/datadog/v2.rb b/lib/capistrano/datadog/v2.rb index 3654a508..5a9f0ffc 100644 --- a/lib/capistrano/datadog/v2.rb +++ b/lib/capistrano/datadog/v2.rb @@ -16,17 +16,25 @@ def find_and_execute_task(path, hooks = {}) task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" result = nil reporter = Capistrano::Datadog.reporter - timing = Benchmark.measure(task.fully_qualified_name) do + task_name = task.fully_qualified_name + timing = Benchmark.measure(task_name) do # Set the current task so that the logger knows which task to # associate the logs with - reporter.current_task = task.fully_qualified_name + reporter.current_task = task_name trigger(hooks[:before], task) if hooks[:before] result = execute_task(task) trigger(hooks[:after], task) if hooks[:after] reporter.current_task = nil end - # Collect the timing in a list for later reporting - reporter.record_task task, timing + + # Record the task name, its timing and roles + roles = task.options[:roles] + if roles.is_a? Proc + roles = roles.call + end + reporter.record_task(task_name, timing.real, roles) + + # Return the original result result end end @@ -58,21 +66,7 @@ def puts(message) namespace :datadog do desc "Submit the tasks that have run to Datadog as events" task :submit do |ns| - begin - api_key = variables[:datadog_api_key] - if api_key - dog = Dogapi::Client.new(api_key) - Datadog::reporter.report.each do |event| - dog.emit_event event - end - else - puts "No api key set, not submitting to Datadog" - end - rescue Timeout::Error => e - puts "Could not submit to Datadog, request timed out." - rescue => e - puts "Could not submit to Datadog: #{e.inspect}\n#{e.backtrace.join("\n")}" - end + Capistrano::Datadog.submit variables[:datadog_api_key] end end end diff --git a/lib/capistrano/datadog/v3.rb b/lib/capistrano/datadog/v3.rb index 25758a9c..703c3291 100644 --- a/lib/capistrano/datadog/v3.rb +++ b/lib/capistrano/datadog/v3.rb @@ -1,2 +1,24 @@ -# No support yet for Capistrano v3 -puts "No Datadog events will be sent since Datadog currently only supports Capistrano v2. For more info, see https://github.com/DataDog/dogapi-rb/issues/40." +require "benchmark" + +# Capistrano v3 uses Rake's DSL instead of its own + +module Rake + class Task + alias old_invoke invoke + def invoke(*args) + result = nil + reporter = Capistrano::Datadog.reporter + task_name = name + timing = Benchmark.measure(task_name) do + result = old_invoke(*args) + end + reporter.record_task(task_name, timing.real, roles) + result + end + end +end + +at_exit do + api_key = Capistrano::Configuration.env.fetch :datadog_api_key + Capistrano::Datadog.submit api_key +end From bc129f18f24f597f57752f6c63120349ecf5e6bf Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Mon, 6 Jan 2014 19:12:15 -0500 Subject: [PATCH 03/10] monkeypatch sshkit formatter to capture log output --- lib/capistrano/datadog/v3.rb | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/capistrano/datadog/v3.rb b/lib/capistrano/datadog/v3.rb index 703c3291..97bb79a9 100644 --- a/lib/capistrano/datadog/v3.rb +++ b/lib/capistrano/datadog/v3.rb @@ -1,4 +1,5 @@ require "benchmark" +require "sshkit/formatters/pretty" # Capistrano v3 uses Rake's DSL instead of its own @@ -9,6 +10,7 @@ def invoke(*args) result = nil reporter = Capistrano::Datadog.reporter task_name = name + reporter.current_task = task_name timing = Benchmark.measure(task_name) do result = old_invoke(*args) end @@ -18,6 +20,36 @@ def invoke(*args) end end +module Capistrano + module Datadog + class CaptureIO + def initialize(wrapped) + @wrapped = wrapped + end + + def write(*args) + @wrapped.write(*args) + args.each {|arg| Capistrano::Datadog.reporter.record_log(arg) } + end + alias :<< :write + + def close + @wrapped.close + end + end + end +end + +module SSHKit + module Formatter + class Pretty + def initialize(oio) + super(Capistrano::Datadog::CaptureIO.new(oio)) + end + end + end +end + at_exit do api_key = Capistrano::Configuration.env.fetch :datadog_api_key Capistrano::Datadog.submit api_key From e09822227049d4b3012cf2da124315615d4766e5 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 6 Feb 2014 14:57:56 -0500 Subject: [PATCH 04/10] fix api key link --- lib/capistrano/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capistrano/README.md b/lib/capistrano/README.md index 180447ff..56452fa2 100644 --- a/lib/capistrano/README.md +++ b/lib/capistrano/README.md @@ -7,7 +7,7 @@ To set up your Capfile: require "capistrano/datadog" set :datadog_api_key, "my_api_key" -You can find your Datadog API key [here](https://app.datad0g.com/account/settings#api). If you don't have a Datadog account, you can sign up for one [here](http://www.datadoghq.com/). +You can find your Datadog API key [here](https://app.datadoghq.com/account/settings#api). If you don't have a Datadog account, you can sign up for one [here](http://www.datadoghq.com/). `capistrano/datadog` will capture each Capistrano task that that Capfile runs, including the roles that the task applies to and any logging output that it emits and submits them as events to Datadog at the end of the execution of all the tasks. If sending to Datadog fails for any reason, your scripts will still succeed. From f9cfecfa23bea898fd72e88818d09c6f5ad11cd7 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 10:54:12 -0500 Subject: [PATCH 05/10] don't add block delimiters to empty messages --- lib/capistrano/datadog.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index 26546065..a80653d2 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -80,7 +80,9 @@ def report() type = "deploy" alert_type = "success" source_type = "capistrano" - message = "@@@" + "\n" + (@logging_output[name] || []).join('') + "@@@" + message_content = (@logging_output[name] || []).join('') + message = if !message_content.empty? then + "@@@\n#{message_content}@@@" else "" end Dogapi::Event.new(message, :msg_title => title, From c2801403a5953f4c9b961203d3ff6126fad79ba4 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 11:08:02 -0500 Subject: [PATCH 06/10] capture the stage as a tag (thanks @arielo) --- lib/capistrano/datadog.rb | 8 ++++++-- lib/capistrano/datadog/v2.rb | 2 +- lib/capistrano/datadog/v3.rb | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index a80653d2..c14e511d 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -49,11 +49,12 @@ def initialize() @logging_output = {} end - def record_task(task_name, timing, roles) + def record_task(task_name, timing, roles, stage=nil) @tasks << { :name => task_name, :timing => timing, - :roles => roles + :roles => roles, + :stage => stage } end @@ -76,6 +77,9 @@ def report() name = task[:name] roles = Array(task[:roles]).map(&:to_s).sort tags = ["#capistrano"] + (roles.map { |t| '#role:' + t }) + if !task[:stage].nil? and !task[:stage].empty? then + tags << "#stage:#{task[:stage]}" + end title = "%s@%s ran %s on %s with capistrano in %.2f secs" % [user, hostname, name, roles.join(', '), task[:timing]] type = "deploy" alert_type = "success" diff --git a/lib/capistrano/datadog/v2.rb b/lib/capistrano/datadog/v2.rb index 5a9f0ffc..6dde70fe 100644 --- a/lib/capistrano/datadog/v2.rb +++ b/lib/capistrano/datadog/v2.rb @@ -32,7 +32,7 @@ def find_and_execute_task(path, hooks = {}) if roles.is_a? Proc roles = roles.call end - reporter.record_task(task_name, timing.real, roles) + reporter.record_task(task_name, timing.real, roles, task.namespace.variables[:stage]) # Return the original result result diff --git a/lib/capistrano/datadog/v3.rb b/lib/capistrano/datadog/v3.rb index 97bb79a9..f63073cb 100644 --- a/lib/capistrano/datadog/v3.rb +++ b/lib/capistrano/datadog/v3.rb @@ -14,7 +14,8 @@ def invoke(*args) timing = Benchmark.measure(task_name) do result = old_invoke(*args) end - reporter.record_task(task_name, timing.real, roles) + reporter.record_task(task_name, timing.real, roles, + Capistrano::Configuration.env.fetch(:stage)) result end end From dece9220fa83d2ff444a43c461028649228dcf18 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 11:23:05 -0500 Subject: [PATCH 07/10] strip color control characters from cap events (fixes #36) --- lib/capistrano/datadog.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index c14e511d..1487072f 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -86,6 +86,8 @@ def report() source_type = "capistrano" message_content = (@logging_output[name] || []).join('') message = if !message_content.empty? then + # Strip out color control characters + message_content = message_content.gsub(/\e\[(\d+)m/, '') "@@@\n#{message_content}@@@" else "" end Dogapi::Event.new(message, From f18d43fde99faba37e24a7c2e44aa9e7945df1bd Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 11:47:53 -0500 Subject: [PATCH 08/10] fix cap version error message --- lib/capistrano/datadog.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capistrano/datadog.rb b/lib/capistrano/datadog.rb index 1487072f..fea1fba3 100644 --- a/lib/capistrano/datadog.rb +++ b/lib/capistrano/datadog.rb @@ -111,5 +111,5 @@ def report() when :v3 require 'capistrano/datadog/v3' else - puts "Unknown version: {Capistrano::Datadog::cap_version.inspect}" + puts "Unknown version: #{Capistrano::Datadog::cap_version.inspect}" end From 542e483a26f928c35bd929f7f2c00b65a430b6f7 Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 11:56:21 -0500 Subject: [PATCH 09/10] fix formatting "errors" --- lib/capistrano/datadog/v2.rb | 1 - lib/capistrano/datadog/v3.rb | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/capistrano/datadog/v2.rb b/lib/capistrano/datadog/v2.rb index 6dde70fe..980f41d1 100644 --- a/lib/capistrano/datadog/v2.rb +++ b/lib/capistrano/datadog/v2.rb @@ -72,4 +72,3 @@ def puts(message) end end end - diff --git a/lib/capistrano/datadog/v3.rb b/lib/capistrano/datadog/v3.rb index f63073cb..1eef07cc 100644 --- a/lib/capistrano/datadog/v3.rb +++ b/lib/capistrano/datadog/v3.rb @@ -15,7 +15,7 @@ def invoke(*args) result = old_invoke(*args) end reporter.record_task(task_name, timing.real, roles, - Capistrano::Configuration.env.fetch(:stage)) + Capistrano::Configuration.env.fetch(:stage)) result end end @@ -25,12 +25,12 @@ module Capistrano module Datadog class CaptureIO def initialize(wrapped) - @wrapped = wrapped + @wrapped = wrapped end def write(*args) @wrapped.write(*args) - args.each {|arg| Capistrano::Datadog.reporter.record_log(arg) } + args.each { |arg| Capistrano::Datadog.reporter.record_log(arg) } end alias :<< :write From 27cd038dc878266d08bc167b9638f2557f89dccb Mon Sep 17 00:00:00 2001 From: Carlo Cabanilla Date: Thu, 13 Feb 2014 11:58:22 -0500 Subject: [PATCH 10/10] add sample Capfile --- examples/Capfile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/Capfile diff --git a/examples/Capfile b/examples/Capfile new file mode 100644 index 00000000..cf1f43b4 --- /dev/null +++ b/examples/Capfile @@ -0,0 +1,19 @@ +require 'capistrano/setup' +require 'capistrano/datadog' + +set :datadog_api_key, 'my_api_key' +set :stage, :test +# set :format, :pretty +# set :log_level, :info +# set :pty, true + +server "host0", roles: ["thing"] +server "host1", roles: ["other_thing"] + +desc "Hello world" +task :hello do + on roles(:all) do |host| + info capture('echo "$(date): Hello from $(whoami)@$(hostname) !"') + end +end +