frankenstein 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ Frankenstein, or the Modern Prometheus, is a collection of tools and
2
+ patterns to make instrumenting a Ruby application just a little bit simpler.
3
+
4
+
5
+ # Installation
6
+
7
+ It's a gem:
8
+
9
+ gem install frankenstein
10
+
11
+ There's also the wonders of [the Gemfile](http://bundler.io):
12
+
13
+ gem 'frankenstein'
14
+
15
+ If you're the sturdy type that likes to run from git:
16
+
17
+ rake install
18
+
19
+ Or, if you've eschewed the convenience of Rubygems entirely, then you
20
+ presumably know what to do already.
21
+
22
+
23
+ # Usage
24
+
25
+ The following classes are available; please see their documentation for more
26
+ details.
27
+
28
+ * **`Frankenstein::Server`**: a simple Webrick-based HTTP server you can
29
+ easily embed in your application to serve metrics requests.
30
+
31
+ * **`Frankenstein::Request`**: collect [basic
32
+ metrics](https://honeycomb.io/blog/2017/01/instrumentation-the-first-four-things-you-measure/)
33
+ about the requests your service receives and makes.
34
+
35
+
36
+ # Contributing
37
+
38
+ See CONTRIBUTING.md.
39
+
40
+
41
+ # Licence
42
+
43
+ Unless otherwise stated, everything in this repo is covered by the following
44
+ copyright notice:
45
+
46
+ Copyright (C) 2017 Civilized Discourse Contruction Kit Inc.
47
+
48
+ This program is free software: you can redistribute it and/or modify it
49
+ under the terms of the GNU General Public License version 3, as
50
+ published by the Free Software Foundation.
51
+
52
+ This program is distributed in the hope that it will be useful,
53
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
54
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
55
+ GNU General Public License for more details.
56
+
57
+ You should have received a copy of the GNU General Public License
58
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,48 @@
1
+ begin
2
+ require 'git-version-bump'
3
+ rescue LoadError
4
+ nil
5
+ end
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "frankenstein"
9
+
10
+ s.version = GVB.version rescue "0.0.0.1.NOGVB"
11
+ s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+
15
+ s.summary = "or, the Modern Prometheus"
16
+ s.description = <<~EOF
17
+ This is a collection of useful tools to help you in more easily
18
+ instrumenting Ruby applications for the Prometheus monitoring
19
+ system.
20
+ EOF
21
+
22
+ s.authors = ["Matt Palmer"]
23
+ s.email = ["matt.palmer@discourse.org"]
24
+ s.homepage = "https://github.com/discourse/frankenstein"
25
+
26
+ s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
27
+
28
+ s.required_ruby_version = ">= 2.3.0"
29
+
30
+ # prometheus-client provides no guaranteed backwards compatibility,
31
+ # and in fact happily breaks things with no notice, so we're stuck
32
+ # with hard-coding a specific version to avoid unexpected disaster.
33
+ s.add_runtime_dependency "prometheus-client", "0.7.1"
34
+ s.add_runtime_dependency "rack", "~> 2.0"
35
+
36
+ s.add_development_dependency 'bundler'
37
+ s.add_development_dependency 'github-release'
38
+ s.add_development_dependency 'git-version-bump'
39
+ s.add_development_dependency 'guard-rspec'
40
+ s.add_development_dependency 'guard-rubocop'
41
+ s.add_development_dependency 'rack-test'
42
+ s.add_development_dependency 'rake', "~> 12.0"
43
+ s.add_development_dependency 'redcarpet'
44
+ s.add_development_dependency 'rspec'
45
+ s.add_development_dependency 'rubocop'
46
+ s.add_development_dependency 'simplecov'
47
+ s.add_development_dependency 'yard'
48
+ end
@@ -0,0 +1,6 @@
1
+ # Base module for all Frankenstein functionality.
2
+ module Frankenstein; end
3
+
4
+ require 'frankenstein/errors'
5
+ require 'frankenstein/request'
6
+ require 'frankenstein/server'
@@ -0,0 +1,4 @@
1
+ module Frankenstein
2
+ # Base class of all exceptions raised by Frankenstein itself
3
+ class Error < StandardError; end
4
+ end
@@ -0,0 +1,191 @@
1
+ require 'prometheus/client'
2
+
3
+ require 'frankenstein/error'
4
+
5
+ module Frankenstein
6
+ # A common pattern for statistical instrumentation is to capture a few basic
7
+ # numbers for all incoming and outgoing requests to the service. Since this
8
+ # is a common pattern, we can abstract that behaviour into a common class,
9
+ # which simplifies the external interface for maintaining statistics in this
10
+ # common case.
11
+ #
12
+ # For more information on this pattern, see
13
+ # https://honeycomb.io/blog/2017/01/instrumentation-the-first-four-things-you-measure/
14
+ #
15
+ class Request
16
+ # No block was passed to #measure.
17
+ class NoBlockError < Frankenstein::Error; end
18
+
19
+ # Create a new request instrumentation package.
20
+ #
21
+ # A "request", for the purposes of this discussion, is a distinct
22
+ # interaction with an external system, typically either the receipt of some
23
+ # sort of communication from another system which needs a response by this
24
+ # system (the one being instrumented), or else the communication to another
25
+ # system from this one for which we are expecting an answer. Each instance
26
+ # of this class should be used to instrument all requests of a particular
27
+ # type.
28
+ #
29
+ # For each instance of this class, the following metrics will be created:
30
+ #
31
+ # * `<prefix>_requests_total` -- a counter indicating the total number
32
+ # of requests started (initiated or received by the system). Labels on
33
+ # this metric are taken from the label set passed to #measure.
34
+ #
35
+ # * `<prefix>_request_duration_seconds` -- a histogram for the response
36
+ # times of successful responses (that is, where no exception was raised).
37
+ # You can get the count of total successful responses from
38
+ # `<prefix>_request_duration_seconds_count`. Labels on this metric
39
+ # are taken from the labels set generated during the measured run (as
40
+ # generated by manipulating the hash yielded to your block).
41
+ #
42
+ # * `<prefix>_exceptions_total` -- a count of the number of exceptions
43
+ # raised during processing. A label, `class`, indicates the class of
44
+ # the exception raised. Labels on this metric are taken from the
45
+ # label set passed to #measure, along with a special label `class`
46
+ # to indicate the class of the exception raised.
47
+ #
48
+ # * `<prefix>_in_progress_count` -- a gauge indicating how many requests
49
+ # are currently in progress as at the time of the scrape. Labels on this
50
+ # metric are taken from the label set passed to #measure.
51
+ #
52
+ # @param prefix [#to_s] the string that will be prepended to all of the
53
+ # Prometheus metric names generated for this instrumentation. The prefix
54
+ # you choose should include both the application name (typically the
55
+ # first word) as well as a unique identifier for the request type itself.
56
+ # Multiple words should be underscore separated.
57
+ #
58
+ # @param outgoing [Boolean] whether this Request instance is collecting
59
+ # data on incoming requests or outgoing requests (the default, as usually
60
+ # there is one incoming request handler, but there can easily be several
61
+ # outgoing request types). It is only used to customise the metric
62
+ # description text for the metrics, so it's not crucially important.
63
+ #
64
+ # @param description [#to_s] a short explanation of what this is measuring.
65
+ # It should be a singular and indefinite noun phrase, to maximise the
66
+ # chances that it will fit neatly into the generated description text.
67
+ #
68
+ # @param registry [Prometheus::Client::Registry] the client registry in
69
+ # which all the metrics will be created. The default will put all the
70
+ # metrics in the Prometheus Client's default registry, which may or may
71
+ # not be what you're up for. If you're using Frankenstein::Server, you
72
+ # want `stats_server.registry`.
73
+ #
74
+ def initialize(prefix, outgoing: true, description: prefix, registry: Prometheus::Client.registry)
75
+ @requests = registry.counter(:"#{prefix}_requests_total", "Number of #{description} requests #{outgoing ? 'sent' : 'received'}")
76
+ @durations = registry.histogram(:"#{prefix}_request_duration_seconds", "Time taken to #{outgoing ? 'receive' : 'send'} a #{description} response")
77
+ @exceptions = registry.counter(:"#{prefix}_exceptions_total", "Number of exceptions raised by the #{description} code")
78
+ @current = registry.gauge(:"#{prefix}_in_progress_count", "Number of #{description} requests currently in progress")
79
+
80
+ # Prometheus::Client::Gauge doesn't (yet) have a built-in way to
81
+ # atomically "adjust" a gauge, only get the current value and set a
82
+ # new value. To avoid the resulting textbook race condition, we
83
+ # need to wrap the get/set pair of operations in this handy-dandy
84
+ # mutex.
85
+ @mutex = Mutex.new
86
+ end
87
+
88
+ # Instrument an instance of the request.
89
+ #
90
+ # Each time a particular external communication occurs, it should be
91
+ # wrapped by a call to this method. Request-related statistics (that
92
+ # the request has been made or received) are updated before the passed
93
+ # block is executed, and then after the block completes,
94
+ # response-related statistics (duration or exception) are recorded. The
95
+ # number of currently-in-progress instances of the request are also kept
96
+ # track of.
97
+ #
98
+ # @param labels [Hash] a set of labels that can help to differentiate
99
+ # different sorts of requests. These labels are applied to the
100
+ # `<prefix>_requests_total` and `<prefix>_in_progress_count` metrics, as
101
+ # well as the `<prefix>_exceptions_total` metric, if an exception is
102
+ # raised.
103
+ #
104
+ # Don't get too fancy with this label set -- it's unusual that this is
105
+ # actually useful in practice. However it is provided for those unusual
106
+ # cases where it isn't a bad idea. Your go-to solution should be to
107
+ # label the `<prefix>_request_duration_seconds` metric (by modifying the
108
+ # hash yielded to the block you pass to #measure), rather than using
109
+ # this parameter with wild abandon.
110
+ #
111
+ # Serious talk time: I've been there. It seems like a great idea at
112
+ # first, to differentiate requests with lots of labels, but it usually
113
+ # just ends up turning into a giant mess. Primarily, due to the way that
114
+ # Prometheus deals with label sets, if you *ever* use a label on *any* of
115
+ # your requests, you need to set the same label to some value on *all* of
116
+ # your requests. So, unless you can say with certainty that every
117
+ # request you receive will logically have some meaningful value for a
118
+ # given label, you shouldn't use it.
119
+ #
120
+ # **NOTE**: the labelset you specify here will be the default labelset
121
+ # applied to the `<prefix>_request_duration_seconds` metric. If you need
122
+ # to remove a label from the response, use `labels.replace` or
123
+ # `labels.delete` to remove the key.
124
+ #
125
+ # @yield [Hash] the labels that will be applied to the
126
+ # `<Prefix>_request_duration_seconds` metric.
127
+ #
128
+ # In order for your label set to be applied, you must *mutate the
129
+ # hash that is yielded*, rather than overwriting it. That means,
130
+ # for example, that the following code **will not work**:
131
+ #
132
+ # req_stats.measure do |labels|
133
+ # labels = {foo: 'bar', baz: 'wombat'}
134
+ # ...
135
+ #
136
+ # Instead, you need to either set each key one by one, or use the
137
+ # handy-dandy Hash#replace method, like this:
138
+ #
139
+ # req_stats.measure do |labels|
140
+ # labels.replace(foo: 'bar', baz: 'wombat')
141
+ # ...
142
+ #
143
+ # If your labels are not being applied to your response histogram,
144
+ # check for any assignment to the yielded variable. It's *really* easy
145
+ # to do by mistake.
146
+ #
147
+ # **NOTE WELL**: The Prometheus specification (assuming it exists)
148
+ # apparently requires that all of the instances of a given metric
149
+ # have the same set of labels. If you fail to do this, an exception
150
+ # will be raised by Prometheus after the block is executed.
151
+ #
152
+ # @raise [Prometheus::Request::NoBlockError] if you didn't pass a block to
153
+ # call. There's nothing to instrument!
154
+ #
155
+ # @raise [Prometheus::Client::LabelSetValidator::LabelSetError] if you
156
+ # violate any written or unwritten rules about how Prometheus label
157
+ # sets should be constructed.
158
+ #
159
+ # @raise [Exception] any exception raised by the executed block will
160
+ # be re-raised by this method after statistics collection is
161
+ # complete.
162
+ #
163
+ # @return [Object] whatever was returned by the block passed.
164
+ #
165
+ def measure(labels = {})
166
+ start_time = Time.now
167
+
168
+ unless block_given?
169
+ raise NoBlockError,
170
+ "No block passed to #{self.class}#measure"
171
+ end
172
+
173
+ @requests.increment(labels, 1)
174
+ @mutex.synchronize { @current.set(labels, (@current.get(labels) || 0) + 1) }
175
+
176
+ res_labels = labels.dup
177
+
178
+ begin
179
+ yield(res_labels).tap do
180
+ elapsed_time = Time.now - start_time
181
+ @durations.observe(res_labels, elapsed_time)
182
+ end
183
+ rescue Exception => ex
184
+ @exceptions.increment(labels.merge(class: ex.class.to_s), 1)
185
+ raise
186
+ ensure
187
+ @mutex.synchronize { @current.set(labels, @current.get(labels) - 1) }
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,121 @@
1
+ require 'logger'
2
+ require 'prometheus/client'
3
+ require 'prometheus/middleware/collector'
4
+ require 'prometheus/middleware/exporter'
5
+ require 'rack'
6
+ require 'rack/builder'
7
+ require 'rack/handler/webrick'
8
+ require 'rack/deflater'
9
+
10
+ require 'frankenstein/error'
11
+
12
+ module Frankenstein
13
+ # A straightforward Prometheus metrics server.
14
+ #
15
+ # When you're looking to instrument your application which isn't, itself, a
16
+ # HTTP service, you need this class. It spawns a WEBrick server on a port you
17
+ # specify, and gives you a registry to put your metrics into. That's pretty
18
+ # much it.
19
+ #
20
+ # The simplest example possible:
21
+ #
22
+ # stats_server = Frankenstein::Server.new
23
+ # stats_server.run # We are now serving stats on port 8080!
24
+ # counter = stats_server.registry.counter(:seconds_count, "Number of seconds")
25
+ # # Give the counter something to count
26
+ # loop { counter.increment({}) }
27
+ #
28
+ # Now if you hit http://localhost:8080/metrics you should see a counter
29
+ # gradually going up, along with stats about the Frankenstein HTTP server
30
+ # itself. Neato!
31
+ #
32
+ # You can change how the webserver logs, and the port it listens on, with options
33
+ # to #new. If for some reason you need to shut down the webserver manually, use
34
+ # #shutdown.
35
+ #
36
+ class Server
37
+ # Indicate that the server is already running
38
+ class AlreadyRunningError < Frankenstein::Error; end
39
+
40
+ # The instance of Prometheus::Client::Registry that contains the metrics
41
+ # that will be presented by this instance of Frankenstein::Server.
42
+ attr_reader :registry
43
+
44
+ # Create a new server instance.
45
+ #
46
+ # @param port [Integer] the TCP to listen on.
47
+ #
48
+ # @param logger [Logger] send log messages from WEBrick to this logger.
49
+ # If not specified, all log messages will be silently eaten.
50
+ #
51
+ # @param metrics_prefix [#to_s] The prefix to apply to the metrics exposed
52
+ # instrumenting the metrics server itself.
53
+ #
54
+ # @param registry [Prometheus::Client::Registry] if you want to use an existing
55
+ # metrics registry for this server, pass it in here. Otherwise, a new one
56
+ # will be created for you, and be made available via #registry.
57
+ #
58
+ def initialize(port: 8080, logger: nil, metrics_prefix: "frankenstein_server", registry: Prometheus::Client::Registry.new)
59
+ @port = port
60
+ @logger = logger || Logger.new(RbConfig::CONFIG['host_os'] =~ /mingw|mswin/ ? 'NUL' : '/dev/null')
61
+ @metrics_prefix = metrics_prefix
62
+ @registry = registry
63
+
64
+ @op_mutex = Mutex.new
65
+ @op_cv = ConditionVariable.new
66
+ end
67
+
68
+ # Start the server instance running.
69
+ #
70
+ # This method returns once the server is just about ready to start serving
71
+ # requests.
72
+ #
73
+ def run
74
+ @op_mutex.synchronize do
75
+ return AlreadyRunningError if @server
76
+
77
+ @server_thread = Thread.new do
78
+ @op_mutex.synchronize do
79
+ @server = WEBrick::HTTPServer.new(Logger: @logger, BindAddress: nil, Port: @port)
80
+ @server.mount "/", Rack::Handler::WEBrick, app
81
+ @op_cv.signal
82
+ end
83
+ @server.start
84
+ end
85
+ end
86
+
87
+ @op_mutex.synchronize { @op_cv.wait(@op_mutex) until @server }
88
+ end
89
+
90
+ # Terminate a running server instance.
91
+ #
92
+ # If the server isn't currently running, this call is a no-op.
93
+ #
94
+ def shutdown
95
+ @op_mutex.synchronize do
96
+ return nil if @server.nil?
97
+ @server.shutdown
98
+ @server = nil
99
+ @server_thread.join
100
+ @server_thread = nil
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def app
107
+ @app ||= begin
108
+ builder = Rack::Builder.new
109
+ builder.use Rack::Deflater, if: ->(_, _, _, body) { body.any? && body[0].length > 512 }
110
+ builder.use Prometheus::Middleware::Collector,
111
+ registry: @registry,
112
+ metrics_prefix: @metrics_prefix,
113
+ counter_label_builder: ->(_, _) { {} },
114
+ duration_label_builder: ->(_, _) { {} }
115
+ builder.use Prometheus::Middleware::Exporter, registry: @registry
116
+ builder.run ->(_) { [301, { 'Location' => "/metrics", 'Content-Type' => 'text/plain' }, ["Try /metrics"]] }
117
+ builder.to_app
118
+ end
119
+ end
120
+ end
121
+ end
metadata ADDED
@@ -0,0 +1,255 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frankenstein
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Palmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prometheus-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: github-release
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: git-version-bump
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rack-test
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '12.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '12.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: redcarpet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: simplecov
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: yard
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ description: |
210
+ This is a collection of useful tools to help you in more easily
211
+ instrumenting Ruby applications for the Prometheus monitoring
212
+ system.
213
+ email:
214
+ - matt.palmer@discourse.org
215
+ executables: []
216
+ extensions: []
217
+ extra_rdoc_files: []
218
+ files:
219
+ - ".gitignore"
220
+ - ".rubocop.yml"
221
+ - ".travis.yml"
222
+ - ".yardopts"
223
+ - CODE_OF_CONDUCT.md
224
+ - CONTRIBUTING.md
225
+ - LICENCE
226
+ - README.md
227
+ - frankenstein.gemspec
228
+ - lib/frankenstein.rb
229
+ - lib/frankenstein/error.rb
230
+ - lib/frankenstein/request.rb
231
+ - lib/frankenstein/server.rb
232
+ homepage: https://github.com/discourse/frankenstein
233
+ licenses: []
234
+ metadata: {}
235
+ post_install_message:
236
+ rdoc_options: []
237
+ require_paths:
238
+ - lib
239
+ required_ruby_version: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: 2.3.0
244
+ required_rubygems_version: !ruby/object:Gem::Requirement
245
+ requirements:
246
+ - - ">="
247
+ - !ruby/object:Gem::Version
248
+ version: '0'
249
+ requirements: []
250
+ rubyforge_project:
251
+ rubygems_version: 2.5.2
252
+ signing_key:
253
+ specification_version: 4
254
+ summary: or, the Modern Prometheus
255
+ test_files: []