fiveruns-dash-ruby 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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