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 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