fiveruns-dash-ruby 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +68 -0
- data/Rakefile +39 -0
- data/lib/fiveruns/dash.rb +144 -0
- data/lib/fiveruns/dash/configuration.rb +116 -0
- data/lib/fiveruns/dash/exception_recorder.rb +135 -0
- data/lib/fiveruns/dash/host.rb +173 -0
- data/lib/fiveruns/dash/instrument.rb +128 -0
- data/lib/fiveruns/dash/metric.rb +379 -0
- data/lib/fiveruns/dash/recipe.rb +63 -0
- data/lib/fiveruns/dash/reporter.rb +208 -0
- data/lib/fiveruns/dash/scm.rb +126 -0
- data/lib/fiveruns/dash/session.rb +81 -0
- data/lib/fiveruns/dash/store/file.rb +24 -0
- data/lib/fiveruns/dash/store/http.rb +198 -0
- data/lib/fiveruns/dash/threads.rb +24 -0
- data/lib/fiveruns/dash/trace.rb +65 -0
- data/lib/fiveruns/dash/typable.rb +29 -0
- data/lib/fiveruns/dash/update.rb +215 -0
- data/lib/fiveruns/dash/version.rb +86 -0
- data/recipes/jruby.rb +107 -0
- data/recipes/ruby.rb +34 -0
- data/test/collector_communication_test.rb +260 -0
- data/test/configuration_test.rb +97 -0
- data/test/exception_recorder_test.rb +112 -0
- data/test/file_store_test.rb +56 -0
- data/test/fixtures/http_store_test/response.json +6 -0
- data/test/http_store_test.rb +210 -0
- data/test/metric_test.rb +204 -0
- data/test/recipe_test.rb +146 -0
- data/test/reliability_test.rb +60 -0
- data/test/reporter_test.rb +46 -0
- data/test/scm_test.rb +70 -0
- data/test/session_test.rb +49 -0
- data/test/test_helper.rb +96 -0
- data/test/tracing_test.rb +68 -0
- data/test/update_test.rb +42 -0
- data/version.yml +3 -0
- metadata +112 -0
data/README.rdoc
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
= FiveRuns Dash core library for Ruby
|
2
|
+
|
3
|
+
Provides a Ruby API to push metrics to the FiveRuns Dash service, http://dash.fiveruns.com, currently in beta.
|
4
|
+
|
5
|
+
You'll need a Dash account before using this library.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
This library is released as a gem from the official repository at http://github.com/fiveruns/dash-ruby
|
10
|
+
|
11
|
+
sudo gem install fiveruns-dash-ruby --source 'http://gems.github.com'
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
See the Ruby Language Guide http://dash.fiveruns.com/help/ruby.html for information on how to use this library.
|
16
|
+
|
17
|
+
== Authors
|
18
|
+
|
19
|
+
The FiveRuns Development Team & Dash community
|
20
|
+
|
21
|
+
== Dependencies
|
22
|
+
|
23
|
+
* The json gem
|
24
|
+
|
25
|
+
== Platforms
|
26
|
+
|
27
|
+
This library has only been tested on OSX and Linux. The `ruby' recipe (which collects metrics on the Ruby process) currently relies on `ps' -- so will not work on Windows. We're actively looking for contributions to widen the number of platforms we support; please help!
|
28
|
+
|
29
|
+
== Contributing
|
30
|
+
|
31
|
+
As an open source project, we welcome community contributions!
|
32
|
+
|
33
|
+
The best way to contribute is by sending pull requests via GitHub. The official repository for this project is:
|
34
|
+
|
35
|
+
http://github.com/fiveruns/dash-ruby
|
36
|
+
|
37
|
+
== Support
|
38
|
+
|
39
|
+
Please join the dash-users Google group, http://groups.google.com/group/dash-users
|
40
|
+
|
41
|
+
You can also contact us via Twitter, Campfire, or email; see the main help page, http://dash.fiveruns.com/help, for details.
|
42
|
+
|
43
|
+
== License
|
44
|
+
|
45
|
+
# (The FiveRuns License)
|
46
|
+
#
|
47
|
+
# Copyright (c) 2006-2008 FiveRuns Corporation
|
48
|
+
#
|
49
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
50
|
+
# a copy of this software and associated documentation files (the
|
51
|
+
# 'Software'), to deal in the Software without restriction, including
|
52
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
53
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
54
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
55
|
+
# the following conditions:
|
56
|
+
#
|
57
|
+
# The above copyright notice and this permission notice shall be
|
58
|
+
# included in all copies or substantial portions of the Software.
|
59
|
+
#
|
60
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
61
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
62
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
63
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
64
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
65
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
66
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
67
|
+
|
68
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.verbose = true
|
5
|
+
t.test_files = FileList['test/*_test.rb']
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'jeweler'
|
12
|
+
|
13
|
+
Jeweler::Tasks.new do |s|
|
14
|
+
s.name = "dash-ruby"
|
15
|
+
s.rubyforge_project = 'fiveruns'
|
16
|
+
s.summary = "FiveRuns Dash core library for Ruby"
|
17
|
+
s.email = "dev@fiveruns.com"
|
18
|
+
s.homepage = "http://github.com/fiveruns/dash-ruby"
|
19
|
+
s.description = "Provides an API to send metrics to the FiveRuns Dash service"
|
20
|
+
s.authors = ["FiveRuns Development Team"]
|
21
|
+
s.files = FileList['README.rdoc', 'Rakefile', 'version.yml', "{lib,test,recipes,examples}/**/*", ]
|
22
|
+
s.add_dependency 'json'
|
23
|
+
s.add_development_dependency 'shoulda'
|
24
|
+
end
|
25
|
+
rescue LoadError
|
26
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
27
|
+
end
|
28
|
+
|
29
|
+
task :coverage do
|
30
|
+
rm_f "coverage"
|
31
|
+
rm_f "coverage.data"
|
32
|
+
rcov = "rcov --exclude gems --exclude version.rb --sort coverage --text-summary --html -o coverage"
|
33
|
+
system("#{rcov} test/*_test.rb")
|
34
|
+
if ccout = ENV['CC_BUILD_ARTIFACTS']
|
35
|
+
FileUtils.rm_rf '#{ccout}/coverage'
|
36
|
+
FileUtils.cp_r 'coverage', ccout
|
37
|
+
end
|
38
|
+
system "open coverage/index.html" if PLATFORM['darwin']
|
39
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
# TODO remove ActiveSupport dependency
|
3
|
+
require 'activesupport'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'thread'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
$:.unshift(File.dirname(__FILE__))
|
10
|
+
|
11
|
+
# NB: Pre-load ALL Dash files here so we do not accidentally
|
12
|
+
# use ActiveSupport's autoloading.
|
13
|
+
|
14
|
+
module Fiveruns; end
|
15
|
+
|
16
|
+
require 'dash/version'
|
17
|
+
require 'dash/configuration'
|
18
|
+
require 'dash/typable'
|
19
|
+
require 'dash/metric'
|
20
|
+
require 'dash/session'
|
21
|
+
require 'dash/reporter'
|
22
|
+
require 'dash/update'
|
23
|
+
require 'dash/host'
|
24
|
+
require 'dash/scm'
|
25
|
+
require 'dash/exception_recorder'
|
26
|
+
require 'dash/recipe'
|
27
|
+
require 'dash/instrument'
|
28
|
+
require 'dash/threads'
|
29
|
+
require 'dash/trace'
|
30
|
+
require 'dash/store/http'
|
31
|
+
require 'dash/store/file'
|
32
|
+
|
33
|
+
module Fiveruns::Dash
|
34
|
+
|
35
|
+
include Threads
|
36
|
+
|
37
|
+
START_TIME = Time.now.utc
|
38
|
+
|
39
|
+
def self.process_age
|
40
|
+
Time.now.utc - START_TIME
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.logger
|
44
|
+
@logger ||= begin
|
45
|
+
if defined?(RAILS_DEFAULT_LOGGER)
|
46
|
+
RAILS_DEFAULT_LOGGER
|
47
|
+
else
|
48
|
+
Logger.new(STDOUT)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.logger=(logger)
|
54
|
+
@logger = logger
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.configure(options = {})
|
58
|
+
handle_pwd_is_root(caller[0]) if Dir.pwd == '/'
|
59
|
+
configuration.options.update(options)
|
60
|
+
yield configuration if block_given?
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.start(options = {}, &block)
|
64
|
+
configure(options, &block) if block_given?
|
65
|
+
session.start
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.host
|
69
|
+
@host ||= Host.new
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.scm
|
73
|
+
@scm ||= unless configuration.options[:scm] == false
|
74
|
+
SCM.matching(configuration.options[:scm_repo])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class << self
|
79
|
+
attr_accessor :trace_contexts
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.register_recipe(name, options = {}, &block)
|
83
|
+
recipes[name] ||= []
|
84
|
+
recipe = Recipe.new(name, options, &block)
|
85
|
+
if recipes[name].include?(recipe)
|
86
|
+
logger.info "Skipping re-registration of recipe :#{name} #{options.inspect}"
|
87
|
+
else
|
88
|
+
recipes[name] << recipe
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.recipes
|
93
|
+
@recipes ||= {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.trace_contexts
|
97
|
+
@trace_contexts ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
#######
|
101
|
+
private
|
102
|
+
#######
|
103
|
+
|
104
|
+
def self.handle_pwd_is_root(last_method)
|
105
|
+
# We are in a Daemon and don't have a valid PWD. Change the
|
106
|
+
# default SCM repo location based on the caller stack.
|
107
|
+
if last_method =~ /([^:]+):\d+/
|
108
|
+
file = File.dirname($1)
|
109
|
+
configuration.options[:scm_repo] = file
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.session
|
114
|
+
@session ||= Session.new(configuration)
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.configuration
|
118
|
+
@configuration ||= begin
|
119
|
+
load_recipes
|
120
|
+
Configuration.new
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.load_recipes
|
125
|
+
Dir[File.join(File.dirname(__FILE__), '..', '..', 'recipes', '**', '*.rb')].each do |core_recipe|
|
126
|
+
require core_recipe
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
module Context
|
131
|
+
def self.set(value)
|
132
|
+
Thread.current[:fiveruns_dash_context] = value
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.reset
|
136
|
+
Thread.current[:fiveruns_dash_context] = []
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.context
|
140
|
+
Thread.current[:fiveruns_dash_context] ||= []
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Fiveruns::Dash
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
class ConflictError < ::ArgumentError; end
|
6
|
+
|
7
|
+
delegate :each, :to => :metrics
|
8
|
+
|
9
|
+
def self.default_options
|
10
|
+
::Fiveruns::Dash.logger.info "CWD::#{Dir.pwd.inspect}"
|
11
|
+
{:scm_repo => Dir.pwd}
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :options
|
15
|
+
def initialize(options = {})
|
16
|
+
@options = self.class.default_options.merge(options)
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def ready?
|
21
|
+
options[:app]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Optionally add to a recipe if the given version meets
|
25
|
+
# a requirement
|
26
|
+
# Note: Requires RubyGems-compatible version scheme (ie, MAJOR.MINOR.PATCH)
|
27
|
+
#
|
28
|
+
# call-seq:
|
29
|
+
# for_version Rails::Version::CURRENT, ['>=', '2.1.0'] do
|
30
|
+
# # ... code to execute
|
31
|
+
# end
|
32
|
+
def for_version(source, requirement)
|
33
|
+
unless source
|
34
|
+
::Fiveruns::Dash.logger.warn "No version given (to check against #{requirement.inspect}), skipping block"
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
source_version = ::Gem::Version.new(source.to_s)
|
38
|
+
requirement = Array(requirement)
|
39
|
+
requirement_version = ::Gem::Version.new(requirement.pop)
|
40
|
+
comparator = normalize_version_comparator(requirement.shift || :==)
|
41
|
+
yield if source_version.__send__(comparator, requirement_version)
|
42
|
+
end
|
43
|
+
|
44
|
+
def metrics #:nodoc:
|
45
|
+
@metrics ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
def recipes
|
49
|
+
@recipes ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
def ignore_exceptions(&rule)
|
53
|
+
Fiveruns::Dash::ExceptionRecorder.add_ignore_rule(&rule)
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_exceptions_from(*meths, &block)
|
57
|
+
block = block ? block : lambda { }
|
58
|
+
meths.push :exceptions => true
|
59
|
+
Instrument.add(*meths, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Merge in an existing recipe
|
63
|
+
# call-seq:
|
64
|
+
# add_recipe :ruby
|
65
|
+
def add_recipe(name, options = {})
|
66
|
+
if Fiveruns::Dash.recipes[name]
|
67
|
+
Fiveruns::Dash.recipes[name].each do |recipe|
|
68
|
+
if !recipes.include?(recipe) && recipe.matches?(options)
|
69
|
+
recipes << recipe
|
70
|
+
recipe.add_to(self)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
else
|
74
|
+
raise ArgumentError, "No such recipe: #{name}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Lookup metrics for modification by subsequent recipes
|
79
|
+
def modify(criteria = {})
|
80
|
+
metrics.each do |metric|
|
81
|
+
if criteria.all? { |k, v| metric.key[k].to_s == v.to_s }
|
82
|
+
yield metric
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Optionally fired by recipes when included
|
88
|
+
def added
|
89
|
+
yield
|
90
|
+
end
|
91
|
+
|
92
|
+
#######
|
93
|
+
private
|
94
|
+
#######
|
95
|
+
|
96
|
+
def normalize_version_comparator(comparator)
|
97
|
+
comparator.to_s == '=' ? '==' : comparator
|
98
|
+
end
|
99
|
+
|
100
|
+
def method_missing(meth, *args, &block)
|
101
|
+
if (klass = Metric.types[meth])
|
102
|
+
metric = klass.new(*args, &block)
|
103
|
+
metric.recipe = Recipe.current
|
104
|
+
if metrics.include?(metric)
|
105
|
+
Fiveruns::Dash.logger.info "Skipping previously defined metric `#{metric.name}'"
|
106
|
+
else
|
107
|
+
metrics << metric
|
108
|
+
end
|
109
|
+
else
|
110
|
+
super
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Fiveruns
|
4
|
+
module Dash
|
5
|
+
|
6
|
+
class ExceptionRecorder
|
7
|
+
|
8
|
+
RULES = []
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def replacements
|
12
|
+
@replacements ||= begin
|
13
|
+
paths = {
|
14
|
+
:system => /^(#{esc(path_prefixes(system_paths))})/,
|
15
|
+
:gems => /^(#{esc(path_prefixes(system_gempaths, '/gems'))})/
|
16
|
+
}
|
17
|
+
%w(RAILS_ROOT MERB_ROOT).each do |root|
|
18
|
+
const = nil
|
19
|
+
const = Object.const_get(root) if Object.const_defined?(root)
|
20
|
+
paths.merge({ :app => regexp_for_path(const) }) if const
|
21
|
+
end
|
22
|
+
paths
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def system_gempaths
|
27
|
+
`gem environment gempath`
|
28
|
+
end
|
29
|
+
|
30
|
+
def system_paths
|
31
|
+
`ruby -e 'puts $:.reject{|p|p=="."}.join(":")'`
|
32
|
+
end
|
33
|
+
|
34
|
+
def regexp_for_path(path)
|
35
|
+
/^(#{Regexp.escape(Pathname.new(path).cleanpath.to_s)})/
|
36
|
+
end
|
37
|
+
|
38
|
+
def path_prefixes(syspaths, suffix='')
|
39
|
+
syspaths.strip.split(":").collect { |path| Pathname.new(path+suffix).cleanpath.to_s }
|
40
|
+
end
|
41
|
+
|
42
|
+
def esc(path_prefixes)
|
43
|
+
path_prefixes.collect{|path|Regexp.escape(path)}.join('|')
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_ignore_rule(&rule)
|
47
|
+
RULES << rule
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(session)
|
52
|
+
@session = session
|
53
|
+
end
|
54
|
+
|
55
|
+
def ignore_exception?(exception)
|
56
|
+
RULES.any? do |rule|
|
57
|
+
rule.call(exception)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def record(exception, sample=nil)
|
62
|
+
return if ignore_exception? exception
|
63
|
+
|
64
|
+
data = extract_data_from_exception(exception)
|
65
|
+
# Allow the sample data to override the exception's display name.
|
66
|
+
data[:name] = sample.delete(:name) if sample and sample[:name]
|
67
|
+
|
68
|
+
if (matching = existing_exception_for(data))
|
69
|
+
matching[:total] += 1
|
70
|
+
matching
|
71
|
+
else
|
72
|
+
data[:total] = 1
|
73
|
+
data[:sample] = flatten_sample sample
|
74
|
+
exceptions << data
|
75
|
+
data
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def data
|
80
|
+
returning exceptions.dup do
|
81
|
+
reset
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def reset
|
86
|
+
exceptions.clear
|
87
|
+
end
|
88
|
+
|
89
|
+
#######
|
90
|
+
private
|
91
|
+
#######
|
92
|
+
|
93
|
+
def flatten_sample(sample)
|
94
|
+
case sample
|
95
|
+
when Hash
|
96
|
+
Hash[*sample.to_a.flatten.map { |v| v.to_s }]
|
97
|
+
when nil
|
98
|
+
{}
|
99
|
+
else
|
100
|
+
raise ArgumentError, "Exception sample must be a Hash instance"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def existing_exception_for(data)
|
105
|
+
# We detect exception dupes based on the same class name and backtrace.
|
106
|
+
exceptions.detect { |e| data[:name] == e[:name] && data[:backtrace] == e[:backtrace] }
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_data_from_exception(e)
|
110
|
+
{
|
111
|
+
:name => e.class.name,
|
112
|
+
:message => e.message,
|
113
|
+
:backtrace => sanitize(e.backtrace)
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def exceptions
|
118
|
+
@exceptions ||= []
|
119
|
+
end
|
120
|
+
|
121
|
+
def sanitize(backtrace)
|
122
|
+
backtrace.map do |line|
|
123
|
+
line = line.strip
|
124
|
+
line.gsub!('in `', "")
|
125
|
+
line.gsub!("'", "")
|
126
|
+
self.class.replacements.each do |name, pattern|
|
127
|
+
line.gsub!(pattern, "[#{name.to_s.upcase}]")
|
128
|
+
end
|
129
|
+
Pathname.new(line).cleanpath.to_s
|
130
|
+
end.join("\n")
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|