unicorn_metrics 0.1.1

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