unicorn_metrics 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzM4NmFhYWM4ZjU5YjAxMGE0NjA5YWQ0ZTUzMTEwNjI0OGE5YmRmOQ==
5
+ data.tar.gz: !binary |-
6
+ MmYxZDljYTIyZTRkZjAyZjkyYTMzMGZhNzc2ZmUxOTA4NWI3MzM5OA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZjZiNzRjODE2MmIzNzY0YmI1MWRjNjdkMjMwNGUzOWFmY2NlZTIyMGI3MTM1
10
+ MTA3ODU4MTM5ZTZjYTY4OTMzNThiYzFlYzc5MzZmMmU3Mzg2ZWQwMzhhNGQx
11
+ YTY3ZDE2MGFmNDJhYmU2ZDZjYmI1NTQyZTFlYzU2NTFkYTc1NDU=
12
+ data.tar.gz: !binary |-
13
+ ZjhiMDNkZjczNjJmNzQ2Y2JmMDIwYTliODk1MWZhNTU5MTE5ZGVhMDFlMzBk
14
+ MzZmZDc0MTE0YzNkNGQ2ZWI3M2FjYmU5YWNmNmVhODkwMDNlOTA5MjVjZWUx
15
+ MTljNTJlOGM2MGY1NzEyNDg3YTlhOTYxYTg4Mzk1NjU1ODRiZTc=
@@ -0,0 +1,41 @@
1
+ # UnicornMetrics::Counter is an atomic counter that conveniently wraps the Raindrops::Struct
2
+ #
3
+ class UnicornMetrics::Counter
4
+ extend Forwardable
5
+
6
+ attr_reader :name
7
+
8
+ class Stats < Raindrops::Struct.new(:value) ; end
9
+
10
+ # Delegate getter and setter to @stats
11
+ def_instance_delegator :@stats, :value
12
+
13
+ # Provide #increment and #decrement by delegating to @stats
14
+ def_instance_delegator :@stats, :incr_value, :increment
15
+ def_instance_delegator :@stats, :decr_value, :decrement
16
+
17
+ # @param name [String] user-defined name
18
+ def initialize(name)
19
+ @name = name
20
+ @stats = Stats.new
21
+ end
22
+
23
+ # Reset the counter
24
+ def reset
25
+ @stats.value = 0
26
+ end
27
+
28
+ def type
29
+ "counter"
30
+ end
31
+
32
+ # @return [Hash] JSON representation of the object
33
+ def as_json(*)
34
+ {
35
+ name => {
36
+ "type" => type,
37
+ "value" => value
38
+ }
39
+ }
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ module UnicornMetrics::DefaultHttpMetrics
2
+ def register_default_http_counters
3
+ [
4
+ ["responses.2xx", /[2]\d{2}/], ["responses.3xx", /[3]\d{2}/],
5
+ ["responses.4xx", /[4]\d{2}/], ["responses.5xx", /[5]\d{2}/]
6
+ ].each { |c| register(:response_counter, *c) }
7
+ end
8
+
9
+ def register_default_http_timers
10
+ [
11
+ ['requests.GET', 'GET'], ['requests.POST', 'POST'],
12
+ ['requests.DELETE', 'DELETE'], ['requests.HEAD', 'HEAD'],
13
+ ['requests.PUT', 'PUT']
14
+ ].each { |c| register(:request_timer, *c) }
15
+ end
16
+ end
@@ -0,0 +1,116 @@
1
+ # UnicornMetrics::Middleware extends the existing Raindrops::Middleware class
2
+ #
3
+ require 'unicorn_metrics' unless defined?(UnicornMetrics)
4
+ require 'raindrops'
5
+ require 'benchmark'
6
+
7
+ class UnicornMetrics::Middleware < Raindrops::Middleware
8
+
9
+ # @param opts [Hash] options hash
10
+ # @option opts [String] :metrics the HTTP endpoint that exposes the application metrics
11
+ def initialize(app, opts = {})
12
+ @registry = UnicornMetrics::Registry
13
+ @metrics_path = opts[:metrics] || "/metrics"
14
+ super
15
+ end
16
+
17
+ def call(env)
18
+ return metrics_response if env['PATH_INFO'] == @metrics_path
19
+
20
+ response = nil
21
+ time = Benchmark.realtime do
22
+ response = super
23
+ #=> [ status, headers, <#Raindrops::Middleware::Proxy> ]
24
+ # Proxy is a wrapper around the response body
25
+ end
26
+ collect_http_metrics(env, response, time) if UnicornMetrics.http_metrics?
27
+ response
28
+ end
29
+
30
+ private
31
+ def metrics_response
32
+ body = @registry.as_json.merge(raindrops).to_json
33
+
34
+ headers = {
35
+ "Content-Type" => "application/json",
36
+ "Content-Length" => body.size.to_s,
37
+ }
38
+ [ 200, headers, [ body ] ]
39
+ end
40
+
41
+ def collect_http_metrics(env, response, elapsed_time)
42
+ method, path = env['REQUEST_METHOD'], env['PATH_INFO']
43
+ status = response[0]
44
+
45
+ UnicornMetrics::ResponseCounter.notify(status, path)
46
+ UnicornMetrics::RequestTimer.notify(method, path, elapsed_time)
47
+ end
48
+
49
+ # Provide Raindrops::Middleware statistics in the metrics JSON response
50
+ # `@stats` is defined in the Raindrops::Middleware class
51
+
52
+ # * calling - the number of application dispatchers on your machine
53
+ # * writing - the number of clients being written to on your machine
54
+ def raindrops
55
+ {
56
+ "raindrops.calling" => {
57
+ "type" => "gauge",
58
+ "value" => @stats.calling
59
+ },
60
+ "raindrops.writing" => {
61
+ "type" => "gauge",
62
+ "value" => @stats.writing
63
+ }
64
+ }.merge(total_listener_stats)
65
+ end
66
+
67
+ # Supporting additional stats collected by Raindrops for Linux platforms
68
+ # `@tcp` and `@unix` are defined in Raindrops::Middleware
69
+ def total_listener_stats(listeners={})
70
+ if defined?(Raindrops::Linux.tcp_listener_stats)
71
+ listeners.merge!(raindrops_tcp_listener_stats) if @tcp
72
+ listeners.merge!(raindrops_unix_listener_stats) if @unix
73
+ end
74
+ listeners
75
+ end
76
+
77
+ def raindrops_tcp_listener_stats
78
+ hash = {
79
+ "raindrops.tcp.active" => { type: :gauge, value: 0 },
80
+ "raindrops.tcp.queued" => { type: :gauge, value: 0 }
81
+ }
82
+ Raindrops::Linux.tcp_listener_stats(@tcp).each do |_, stats|
83
+ hash["raindrops.tcp.active"][:value] += stats.active.to_i
84
+ hash["raindrops.tcp.queued"][:value] += stats.queued.to_i
85
+ end
86
+ hash
87
+ end
88
+
89
+ def raindrops_unix_listener_stats
90
+ hash = {
91
+ "raindrops.unix.active" => { type: :gauge, value: 0 },
92
+ "raindrops.unix.queued" => { type: :gauge, value: 0 }
93
+ }
94
+ Raindrops::Linux.unix_listener_stats(@unix).each do |_, stats|
95
+ hash["raindrops.unix.active"][:value] += stats.active.to_i
96
+ hash["raindrops.unix.queued"][:value] += stats.queued.to_i
97
+ end
98
+ hash
99
+ end
100
+
101
+ # NOTE: The 'total' is being used in favor of returning stats for \
102
+ # each listening address, which was the default in Raindrops
103
+ def listener_stats(listeners={})
104
+ if defined?(Raindrops::Linux.tcp_listener_stats)
105
+ Raindrops::Linux.tcp_listener_stats(@tcp).each do |addr,stats|
106
+ listeners["raindrops.#{addr}.active"] = "#{stats.active}"
107
+ listeners["raindrops.#{addr}.queued"] = "#{stats.queued}"
108
+ end if @tcp
109
+ Raindrops::Linux.unix_listener_stats(@unix).each do |addr,stats|
110
+ listeners["raindrops.#{addr}.active"] = "#{stats.active}"
111
+ listeners["raindrops.#{addr}.queued"] = "#{stats.queued}"
112
+ end if @unix
113
+ end
114
+ listeners
115
+ end
116
+ end
@@ -0,0 +1,75 @@
1
+ # Borrowing nomenclature from http://metrics.codahale.com/
2
+ #
3
+ # To support a cleaner interface, the UnicornMetrics module delegates to the Registry
4
+ # for supported methods. Methods should not be called directly on this module
5
+
6
+ # UnicornMetrics::Registry is a container for Metrics
7
+ # @private
8
+ module UnicornMetrics::Registry
9
+
10
+ # Map metrics types to class names
11
+ METRIC_TYPES = {
12
+ :counter => 'Counter',
13
+ :timer => 'Timer',
14
+ :response_counter => 'ResponseCounter',
15
+ :request_counter => 'RequestCounter',
16
+ :request_timer => 'RequestTimer'
17
+ }
18
+
19
+ class << self
20
+
21
+ # Return a hash of metrics that have been defined
22
+ #
23
+ # @return [Hash] a metric name to metric object
24
+ def metrics
25
+ @metrics ||= {}
26
+ end
27
+
28
+ # Register a new metric. Arguments are optional. See metric class definitions.
29
+ #
30
+ # @param type [Symbol] underscored metric name
31
+ # @param name [String] string representing the metric name
32
+ # @return [Counter, Timer, ResponseCounter, RequestCounter, RequestTimer]
33
+ def register(type, name, *args)
34
+ type = METRIC_TYPES.fetch(type) { raise "Invalid type: #{type}" }
35
+ validate_name!(name)
36
+ metric = UnicornMetrics.const_get(type).new(name,*args)
37
+ metrics[name] = metric
38
+ define_getter(name)
39
+
40
+ metric
41
+ end
42
+
43
+ # @return [Hash] default JSON representation of metrics
44
+ def as_json(*)
45
+ metrics.inject({}) do |hash, (name, metric)|
46
+ hash.merge(metric.as_json)
47
+ end
48
+ end
49
+
50
+ private
51
+ # Convenience methods to return the stored metrics.
52
+ # Allows the use of names with spaces, dots, and dashes, which are \
53
+ # replaced by an underscore:
54
+ #
55
+ # def UnicornMetrics::Registry.stat_name
56
+ # metrics.fetch('stat_name')
57
+ # end
58
+ #
59
+ def define_getter(name)
60
+ define_singleton_method(format_name(name)) { metrics.fetch(name) }
61
+ end
62
+
63
+ # Replace non-word characters with '_'
64
+ def format_name(name)
65
+ name.gsub(/\W/, '_')
66
+ end
67
+
68
+ # @raise [ArgumentError] if the metric name is in use
69
+ def validate_name!(name)
70
+ if metrics.fetch(name,false)
71
+ raise ArgumentError, "The name, '#{name}', is in use."
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,42 @@
1
+ # Counter defined to keep count of method types of http requests
2
+ # Requires the UnicornMetrics::Middleware
3
+
4
+ class UnicornMetrics::RequestCounter < UnicornMetrics::Counter
5
+ attr_reader :path, :method_name
6
+
7
+ METHOD_COUNTERS = []
8
+
9
+ # @param name [String] user-defined name
10
+ # @param method_name [String] name of the HTTP method
11
+ # @param path [Regex] optional regex that is used to match to a specific URI
12
+ def initialize(name, method_name, path=nil)
13
+ @path = path
14
+ @method_name = method_name.to_s.upcase
15
+ METHOD_COUNTERS << self
16
+ super(name)
17
+ end
18
+
19
+ # @return [Array<UnicornMetrics::RequestCounter>]
20
+ def self.counters ; METHOD_COUNTERS ; end
21
+
22
+ # @param meth_val [String] is the HTTP method of the request
23
+ # @param path [String] is the URI of the request
24
+ def self.notify(meth_val, path)
25
+ counters.each { |c| c.increment if c.path_method_match?(meth_val, path) }
26
+ end
27
+
28
+ # @param (see #notify)
29
+ # @return [Boolean]
30
+ def path_method_match?(meth_val, path_val)
31
+ path_matches?(path_val) && method_matches?(meth_val)
32
+ end
33
+
34
+ private
35
+ def path_matches?(val)
36
+ !!(path =~ val) || path.nil?
37
+ end
38
+
39
+ def method_matches?(val)
40
+ method_name.upcase == val.to_s
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ # Timer defined to keep track of total elapsed request time
2
+ # Requires the UnicornMetrics::Middleware
3
+
4
+ class UnicornMetrics::RequestTimer < UnicornMetrics::Timer
5
+ attr_reader :path, :method_name
6
+
7
+ REQUEST_TIMERS = []
8
+
9
+ # @param name [String] user-defined name
10
+ # @param method_name [String] name of the HTTP method
11
+ # @param path [Regex] optional regex that is used to match to a specific URI
12
+ def initialize(name, method_name, path=nil)
13
+ @path = path
14
+ @method_name = method_name.to_s
15
+ REQUEST_TIMERS << self
16
+ super(name)
17
+ end
18
+
19
+ # @return [Array<UnicornMetrics::RequestTimer>]
20
+ def self.timers ; REQUEST_TIMERS ; end
21
+
22
+ # @param meth_val [String] is the HTTP method of the request
23
+ # @param path [String] is the URI of the request
24
+ def self.notify(meth_val, path, elapsed_time)
25
+ timers.each { |c| c.tick(elapsed_time) if c.path_method_match?(meth_val, path) }
26
+ end
27
+
28
+
29
+ # @param (see #notify)
30
+ # @return [Boolean]
31
+ def path_method_match?(meth_val, path_val)
32
+ path_matches?(path_val) && method_matches?(meth_val)
33
+ end
34
+
35
+ private
36
+ def path_matches?(val)
37
+ !!(path =~ val) || path.nil?
38
+ end
39
+
40
+ def method_matches?(val)
41
+ method_name.upcase == val.to_s
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # Counter defined to keep count of status codes of http responses
2
+ # Requires the UnicornMetrics::Middleware
3
+
4
+ class UnicornMetrics::ResponseCounter < UnicornMetrics::Counter
5
+ attr_reader :path, :status_code
6
+
7
+ STATUS_COUNTERS = []
8
+
9
+ # @param name [String] user-defined name
10
+ # @param status_code [Regex] the HTTP status code (e.g., `/[2]\d{2}/`)
11
+ # @param path [Regex] optional regex that is used to match to a specific URI
12
+ def initialize(name, status_code, path=nil)
13
+ @path = path
14
+ @status_code = status_code
15
+ STATUS_COUNTERS << self
16
+ super(name)
17
+ end
18
+
19
+ # @return [Array<UnicornMetrics::ResponseCounter>]
20
+ def self.counters ; STATUS_COUNTERS ; end
21
+
22
+
23
+ # @param status [String] is the HTTP status code of the request
24
+ # @param path [String] is the URI of the request
25
+ def self.notify(status, path)
26
+ counters.each { |c| c.increment if c.path_status_match?(status, path) }
27
+ end
28
+
29
+ # @param (see #notify)
30
+ # @return [Boolean]
31
+ def path_status_match?(status,path)
32
+ status_matches?(status) && path_matches?(path)
33
+ end
34
+
35
+ private
36
+ def path_matches?(val)
37
+ path.nil? || !!(path =~ val)
38
+ end
39
+
40
+ def status_matches?(val)
41
+ !!(status_code =~ val.to_s)
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ # UnicornMetrics::Timer keeps track of total time and the count of 'ticks'
2
+ # A simple rate of average of ticks over time elapsed can be calculated this way.
3
+ # For more advanced metrics (e.g., 1/5/15min moving averages) this data should be reported to an intelligent metric store (i.e. Graphite)
4
+ #
5
+ class UnicornMetrics::Timer
6
+ extend Forwardable
7
+
8
+ attr_reader :name
9
+
10
+ # The Raindrops::Struct can only hold unsigned long ints (0 -> 4,294,967,295)
11
+ # Since we usually care about ms in a web application, \
12
+ # let's store 3 significant digits after the decimal
13
+ EXPONENT = -3
14
+
15
+ class Stats < Raindrops::Struct.new(:count, :mantissa) ; end
16
+
17
+ def_instance_delegators :@stats, :mantissa, :count
18
+
19
+ # @param name [String] user-defined name
20
+ def initialize(name)
21
+ @name = name
22
+ @stats = Stats.new
23
+ end
24
+
25
+ def type
26
+ "timer"
27
+ end
28
+
29
+ # @param elapsed_time [Numeric] in seconds
30
+ def tick(elapsed_time)
31
+ elapsed_time = (elapsed_time * 10**-EXPONENT).to_i
32
+
33
+ @stats.mantissa = mantissa + elapsed_time
34
+ @stats.incr_count
35
+ end
36
+
37
+ # Reset the timer
38
+ def reset
39
+ @stats.mantissa = 0 and @stats.count = 0
40
+ end
41
+
42
+ # @return [Numeric] total elapsed time
43
+ def sum
44
+ (mantissa * 10**EXPONENT).to_f.round(-EXPONENT)
45
+ end
46
+
47
+ # @return [Hash] JSON representation of the object
48
+ def as_json(*)
49
+ {
50
+ name => {
51
+ "type" => type,
52
+ "sum" => sum,
53
+ "count" => count
54
+ }
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module UnicornMetrics
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,62 @@
1
+ module UnicornMetrics
2
+ class << self
3
+
4
+ # Returns the UnicornMetrics::Registry object
5
+ #
6
+ # @return [UnicornMetrics::Registry]
7
+ def registry
8
+ UnicornMetrics::Registry
9
+ end
10
+
11
+ # Make this class 'configurable'
12
+ #
13
+ # @yieldparam self [UnicornMetrics]
14
+ def configure
15
+ yield self
16
+ end
17
+
18
+ # Enable/disable HTTP metrics. Includes defaults
19
+ #
20
+ # @param boolean [Boolean] to enable or disable default HTTP metrics
21
+ def http_metrics=(boolean=false)
22
+ return if @_assigned
23
+
24
+ if @http_metrics = boolean
25
+ registry.extend(UnicornMetrics::DefaultHttpMetrics)
26
+ registry.register_default_http_counters
27
+ registry.register_default_http_timers
28
+ end
29
+ @_assigned = true
30
+ end
31
+
32
+ # Used by the middleware to determine whether any HTTP metrics have been defined
33
+ #
34
+ # @return [Boolean] if HTTP metrics have been defined
35
+ def http_metrics? ; @http_metrics ; end
36
+
37
+ private
38
+ # Delegate methods to UnicornMetrics::Registry
39
+ #
40
+ # http://robots.thoughtbot.com/post/28335346416/always-define-respond-to-missing-when-overriding
41
+ def respond_to_missing?(method_name, include_private=false)
42
+ registry.respond_to?(method_name, include_private)
43
+ end
44
+
45
+ def method_missing(method_name, *args, &block)
46
+ return super unless registry.respond_to?(method_name)
47
+ registry.send(method_name, *args, &block)
48
+ end
49
+ end
50
+ end
51
+
52
+ require 'raindrops'
53
+ require 'unicorn_metrics/registry'
54
+ require 'unicorn_metrics/version'
55
+ require 'unicorn_metrics/counter'
56
+ require 'unicorn_metrics/timer'
57
+ require 'unicorn_metrics/default_http_metrics'
58
+ require 'unicorn_metrics/request_counter'
59
+ require 'unicorn_metrics/request_timer'
60
+ require 'unicorn_metrics/response_counter'
61
+ require 'forwardable'
62
+
@@ -0,0 +1,65 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::Counter do
4
+ before do
5
+ @counter = UnicornMetrics::Counter.new("test_counter")
6
+ @counter.reset
7
+ end
8
+
9
+ describe "#type" do
10
+ it "returns 'counter'" do
11
+ @counter.type.must_equal 'counter'
12
+ end
13
+ end
14
+
15
+ describe "#value" do
16
+ it "returns the internal count" do
17
+ @counter.value.must_equal 0
18
+ end
19
+ end
20
+
21
+ describe "#increment" do
22
+ it "increments the counter value" do
23
+ 5.times { @counter.increment }
24
+ @counter.value.must_equal 5
25
+ end
26
+ end
27
+
28
+ describe "#decrement" do
29
+ it "decrements the counter value" do
30
+ 5.times { @counter.increment }
31
+ 5.times { @counter.decrement }
32
+ @counter.value.must_equal 0
33
+ end
34
+ end
35
+
36
+ describe "#reset" do
37
+ it "resets the counter value" do
38
+ 5.times { @counter.increment }
39
+ @counter.reset
40
+ @counter.value.must_equal 0
41
+ end
42
+ end
43
+
44
+ describe "#as_json" do
45
+ it "returns the JSON representation of the object as a hash" do
46
+ hash = {
47
+ @counter.name => {
48
+ "type" => @counter.type,
49
+ "value" => @counter.value
50
+ }
51
+ }
52
+
53
+ @counter.as_json.must_equal hash
54
+ end
55
+ end
56
+
57
+ # REFACTOR: This test is very slow
58
+ describe "forking" do
59
+ it "can be shared across processes" do
60
+ 2.times { fork { @counter.increment ; exit } }
61
+ Process.waitall
62
+ @counter.value.must_equal 2
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
3
+ require 'unicorn_metrics'
4
+
5
+ alias :context :describe
@@ -0,0 +1,61 @@
1
+ require 'test_helper'
2
+ require 'unicorn_metrics/middleware'
3
+ require 'json'
4
+
5
+ # Stubbing Raindrops::Linux to support testing listener statistics
6
+ # See Raindrops::Middleware and Raindrops::Linux
7
+ module Raindrops::Linux
8
+ Stats = Struct.new(:active, :queued)
9
+ def self.tcp_listener_stats(*) ; [['123', Stats.new(1,5)]] ; end
10
+ def self.unix_listener_stats(*) ; [['456', Stats.new(1,5)]] ; end
11
+ end
12
+
13
+ describe UnicornMetrics::Middleware do
14
+ before do
15
+ @resp_headers = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
16
+ @response = [ 200, @resp_headers, ["test_body"] ]
17
+ @app = ->(env){ @response }
18
+
19
+ # Remove any metrics lingering from previous tests
20
+ UnicornMetrics.metrics.delete_if{true}
21
+
22
+ @counter = UnicornMetrics.register(:counter, "test_counter")
23
+ options = { metrics: '/metrics', listeners: %w(0.0.0.0:80) }
24
+ @middleware = UnicornMetrics::Middleware.new(@app, options)
25
+ end
26
+
27
+ after { UnicornMetrics.metrics.delete("test_counter")}
28
+
29
+ describe "#call" do
30
+ context "when path matches the defined metrics path" do
31
+ before do
32
+ response = @middleware.call({'PATH_INFO' => '/metrics'})
33
+ @hash = JSON response[2][0]
34
+ end
35
+
36
+ it "returns the metrics response JSON body" do
37
+ @hash.fetch("test_counter").must_equal @counter.as_json.fetch("test_counter")
38
+ end
39
+
40
+ it "includes raindrops middleware metrics" do
41
+ @hash.must_include "raindrops.calling"
42
+ @hash.must_include "raindrops.writing"
43
+ @hash.must_include "raindrops.tcp.active"
44
+ @hash.must_include "raindrops.tcp.queued"
45
+ end
46
+ end
47
+
48
+ context "when the path does not match the defined metrics path" do
49
+ it "returns the expected response" do
50
+ response = @middleware.call({'PATH_INFO' => '/'})
51
+
52
+ # The Raindrops::Middleware wraps the response body in a Proxy
53
+ # Write the response body to a string to match the expectation
54
+ response[2] = [ response[2].inject(""){ |str, v| str << v } ]
55
+
56
+ response.must_equal @response
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::Registry do
4
+ describe "METRIC_TYPES" do
5
+ it "returns a hash that maps type symbols to class names" do
6
+ hash = {
7
+ :counter => 'Counter',
8
+ :timer => 'Timer',
9
+ :response_counter => 'ResponseCounter',
10
+ :request_counter => 'RequestCounter',
11
+ :request_timer => 'RequestTimer'
12
+ }
13
+ UnicornMetrics::Registry::METRIC_TYPES.must_equal hash
14
+ end
15
+ end
16
+
17
+ describe ".register" do
18
+ before { UnicornMetrics.register(:counter,"test-counter") }
19
+ after { UnicornMetrics.metrics.delete("test-counter")}
20
+
21
+ it "initializes and stores a new metric object" do
22
+ UnicornMetrics.metrics.fetch("test-counter").must_be_instance_of UnicornMetrics::Counter
23
+ end
24
+
25
+ it "defines getter method from the name of the metric with non-word chars replaced by '_'" do
26
+ UnicornMetrics.metrics.fetch("test-counter").must_be_same_as UnicornMetrics.test_counter
27
+ end
28
+
29
+ it "raises an error if a name is used twice" do
30
+ ->{UnicornMetrics.register(:counter, "test-counter")}.must_raise ArgumentError
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::RequestCounter do
4
+ before do
5
+ @counter = UnicornMetrics::RequestCounter.new("test_counter", 'POST')
6
+ @counter.reset
7
+ end
8
+
9
+ describe ".counters" do
10
+ it "returns a collection of current RequestCounter instances" do
11
+ UnicornMetrics::RequestCounter.counters.must_include @counter
12
+ end
13
+ end
14
+
15
+ describe ".notify" do
16
+ it "increments all existing counters that match an http method and path" do
17
+ UnicornMetrics::RequestCounter.notify('POST','/')
18
+ @counter.value.must_equal 1
19
+ end
20
+ end
21
+
22
+ describe "#path_method_match?" do
23
+ context "when path is nil (not specified)" do
24
+ context "when method name matches" do
25
+ it "returns true" do
26
+ @counter.path_method_match?('POST', '/anything').must_equal true
27
+ end
28
+ end
29
+
30
+ context "when method name does not match" do
31
+ it "returns false" do
32
+ @counter.path_method_match?('GET', '/anything').must_equal false
33
+ end
34
+ end
35
+ end
36
+
37
+ context "when path is not nil (it is set)" do
38
+ before { @counter.instance_variable_set(:@path, /\/something/) }
39
+ after { @counter.instance_variable_set(:@path, nil) }
40
+
41
+ context "when method matches" do
42
+ context "when patch matches" do
43
+ it "returns true" do
44
+ @counter.path_method_match?('POST', '/something').must_equal true
45
+ end
46
+ end
47
+
48
+ context "when patch does not match" do
49
+ it "returns false" do
50
+ @counter.path_method_match?('POST', '/bla').must_equal false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::RequestTimer do
4
+ before do
5
+ @timer = UnicornMetrics::RequestTimer.new("test_timer", 'POST')
6
+ @timer.reset
7
+ end
8
+
9
+ describe ".timers" do
10
+ it "returns a collection of current RequestTimer instances" do
11
+ UnicornMetrics::RequestTimer.timers.must_include @timer
12
+ end
13
+ end
14
+
15
+ describe ".notify" do
16
+ it "ticks all existing timers that match an http method and path" do
17
+ UnicornMetrics::RequestTimer.notify('POST','/', 10.0)
18
+ @timer.sum.must_equal 10.0
19
+ end
20
+ end
21
+
22
+ describe "#path_method_match?" do
23
+ context "when path is nil (not specified)" do
24
+ context "when method name matches" do
25
+ it "returns true" do
26
+ @timer.path_method_match?('POST', '/anything').must_equal true
27
+ end
28
+ end
29
+
30
+ context "when method name does not match" do
31
+ it "returns false" do
32
+ @timer.path_method_match?('GET', '/anything').must_equal false
33
+ end
34
+ end
35
+ end
36
+
37
+ context "when path is not nil (it is set)" do
38
+ before { @timer.instance_variable_set(:@path, /\/something/) }
39
+ after { @timer.instance_variable_set(:@path, nil) }
40
+
41
+ context "when method matches" do
42
+ context "when patch matches" do
43
+ it "returns true" do
44
+ @timer.path_method_match?('POST', '/something').must_equal true
45
+ end
46
+ end
47
+
48
+ context "when patch does not match" do
49
+ it "returns false" do
50
+ @timer.path_method_match?('POST', '/bla').must_equal false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::ResponseCounter do
4
+ before do
5
+ @counter = UnicornMetrics::ResponseCounter.new("test_counter", /[2]\d{2}/)
6
+ @counter.reset
7
+ end
8
+
9
+ describe ".counters" do
10
+ it "returns a collection of current ResponseCounter instances" do
11
+ UnicornMetrics::ResponseCounter.counters.must_include @counter
12
+ end
13
+ end
14
+
15
+ describe ".notify" do
16
+ it "increments all existing counters that match a status code and path" do
17
+ UnicornMetrics::ResponseCounter.notify('200','/')
18
+ @counter.value.must_equal 1
19
+ end
20
+ end
21
+
22
+ describe "#path_status_match?" do
23
+ context "when path is nil (not specified)" do
24
+ context "when status name matches" do
25
+ it "returns true" do
26
+ @counter.path_status_match?('200', '/anything').must_equal true
27
+ end
28
+ end
29
+
30
+ context "when status name does not match" do
31
+ it "returns false" do
32
+ @counter.path_status_match?('400', '/anything').must_equal false
33
+ end
34
+ end
35
+ end
36
+
37
+ context "when path is not nil (it is set)" do
38
+ before { @counter.instance_variable_set(:@path, /\/something/) }
39
+ after { @counter.instance_variable_set(:@path, nil) }
40
+
41
+ context "when status matches" do
42
+ context "when patch matches" do
43
+ it "returns true" do
44
+ @counter.path_status_match?('200', '/something').must_equal true
45
+ end
46
+ end
47
+
48
+ context "when patch does not match" do
49
+ it "returns false" do
50
+ @counter.path_status_match?('200', '/bla').must_equal false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics::Timer do
4
+ before do
5
+ @timer = UnicornMetrics::Timer.new("test_timer")
6
+ @timer.reset
7
+ end
8
+
9
+ describe "#type" do
10
+ it "returns 'timer'" do
11
+ @timer.type.must_equal 'timer'
12
+ end
13
+ end
14
+
15
+ context "when initialized" do
16
+ describe "#sum" do
17
+ it "must be zero" do
18
+ @timer.sum.must_equal 0.0
19
+ end
20
+ end
21
+
22
+ describe "#count" do
23
+ it "must be zero" do
24
+ @timer.count.must_equal 0
25
+ end
26
+ end
27
+ end
28
+
29
+ context "when ticked" do
30
+ describe "#sum" do
31
+ it "returns sum + elapsed time" do
32
+ @timer.tick(5)
33
+ @timer.sum.must_equal 5.0
34
+ end
35
+ end
36
+
37
+ describe "#count" do
38
+ it "returns the count of ticks" do
39
+ @timer.tick(5)
40
+ @timer.count.must_equal 1
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "#reset" do
46
+ it "resets count and sum" do
47
+ 5.times { @timer.tick(5) }
48
+ @timer.reset
49
+ @timer.sum.must_equal 0
50
+ @timer.count.must_equal 0
51
+ end
52
+ end
53
+
54
+ describe "#as_json" do
55
+ it "returns the JSON representation of the object as a hash" do
56
+ hash = {
57
+ @timer.name => {
58
+ "type" => @timer.type,
59
+ "sum" => @timer.sum,
60
+ "count" => @timer.count
61
+ }
62
+ }
63
+
64
+ @timer.as_json.must_equal hash
65
+ end
66
+ end
67
+
68
+ describe "forking" do
69
+ it "can be shared across processes" do
70
+ 2.times { fork { @timer.tick(5) ; exit } }
71
+ Process.waitall
72
+ @timer.sum.must_equal 10.0
73
+ @timer.count.must_equal 2
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ describe UnicornMetrics do
4
+ describe "::registry" do
5
+ it "returns the UnicornMetrics::Registry object" do
6
+ UnicornMetrics.registry.must_equal UnicornMetrics::Registry
7
+ end
8
+ end
9
+
10
+ describe "::configure" do
11
+ it "yields self" do
12
+ ->{ UnicornMetrics.configure {|u| print u}}.must_output 'UnicornMetrics'
13
+ end
14
+ end
15
+
16
+ describe "::http_metrics=" do
17
+ context "when arg is false" do
18
+ it "should not extend Registry with DefaultHttpCounters module" do
19
+ UnicornMetrics.registry.wont_respond_to :register_default_http_counters
20
+ end
21
+ end
22
+
23
+ context "when arg is true" do
24
+ before { UnicornMetrics.http_metrics = true }
25
+
26
+ it "extends Registry with DefaultHttpMetrics module" do
27
+ UnicornMetrics.registry.must_respond_to :register_default_http_counters
28
+ UnicornMetrics.registry.must_respond_to :register_default_http_timers
29
+ end
30
+
31
+ it "registers the default http counters" do
32
+ UnicornMetrics.registry.metrics.keys.size.must_equal 9
33
+ end
34
+ end
35
+ end
36
+
37
+ it "delegates unknown methods to Registry" do
38
+ methods = UnicornMetrics.registry.methods(false)
39
+ respond_count = 0
40
+ methods.each { |m| respond_count+=1 if UnicornMetrics.respond_to?(m) }
41
+ respond_count.must_equal methods.size
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unicorn_metrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Alan Cohen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 10.1.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 10.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: raindrops
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.11.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.11.0
41
+ description:
42
+ email:
43
+ - acohen@climate.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/unicorn_metrics/counter.rb
49
+ - lib/unicorn_metrics/default_http_metrics.rb
50
+ - lib/unicorn_metrics/middleware.rb
51
+ - lib/unicorn_metrics/registry.rb
52
+ - lib/unicorn_metrics/request_counter.rb
53
+ - lib/unicorn_metrics/request_timer.rb
54
+ - lib/unicorn_metrics/response_counter.rb
55
+ - lib/unicorn_metrics/timer.rb
56
+ - lib/unicorn_metrics/version.rb
57
+ - lib/unicorn_metrics.rb
58
+ - test/test_counter.rb
59
+ - test/test_helper.rb
60
+ - test/test_middleware.rb
61
+ - test/test_registry.rb
62
+ - test/test_request_counter.rb
63
+ - test/test_request_timer.rb
64
+ - test/test_response_counter.rb
65
+ - test/test_timer.rb
66
+ - test/test_unicorn_metrics.rb
67
+ homepage: http://www.climate.com
68
+ licenses: []
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: 1.9.3
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements:
85
+ - Preforking http server (i.e., Unicorn).
86
+ rubyforge_project:
87
+ rubygems_version: 2.0.6
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Metrics library for Rack applications using a preforking http server (i.e.,
91
+ Unicorn)
92
+ test_files:
93
+ - test/test_counter.rb
94
+ - test/test_helper.rb
95
+ - test/test_middleware.rb
96
+ - test/test_registry.rb
97
+ - test/test_request_counter.rb
98
+ - test/test_request_timer.rb
99
+ - test/test_response_counter.rb
100
+ - test/test_timer.rb
101
+ - test/test_unicorn_metrics.rb
102
+ has_rdoc: