epsagon 0.0.11 → 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab0dc77590ae9d619fa9dc901ce8af447cfa4555b3e4e2de648952add1604043
4
- data.tar.gz: ba0e5a0589ac967734b3019e229031a345577cc44518d876228238011f3bd2a4
3
+ metadata.gz: 808ee2781331d674be9e661fdb982402a9fa118a7e530226334dac758ce80dbb
4
+ data.tar.gz: 370aad1ea87d60e10ffa0b0c6777951c290aaacb2074a6556b6b77a41f9606d3
5
5
  SHA512:
6
- metadata.gz: d383f183f674c601ef695e5b47adcaa770e8d9d6cfbe22d922b4e8785dc2b6f5c677ec0abfaab7c01df880afabe8b3c0395ffc076ff67fe3393ecc06f8a37cd5
7
- data.tar.gz: 895b8b35143b0c232e4e3611ddf32652163a191f4d58af3283101af3f2f107f396be765931c3734f5f3aebe6cf2ec4f2d19d561b4cc033286cc1a33b4f373fd2
6
+ metadata.gz: 223f608c05ddf216c1db46709d234604af81dca746fe16d5c4f24e89618450313c6e8a972f35fa2c2f5c6aa4b5811cd97e638d625dde9a1d5b0bc9a00575cf92
7
+ data.tar.gz: 5317d3432c01890de86919a5415322ed151002d12058027fa890cffa34ee32818d3e87e1d4484b5317aa1ab7c9047bf9347c2d5cf71e5718791ac6766e0163eb
data/lib/epsagon.rb CHANGED
@@ -10,19 +10,22 @@ require_relative 'instrumentation/sinatra'
10
10
  require_relative 'instrumentation/net_http'
11
11
  require_relative 'instrumentation/faraday'
12
12
  require_relative 'instrumentation/aws_sdk'
13
+ require_relative 'instrumentation/rails'
13
14
  require_relative 'util'
14
15
 
15
16
  Bundler.require
16
17
 
17
18
  # Epsagon tracing main entry point
18
19
  module Epsagon
20
+ DEFAULT_BACKEND = 'opentelemetry.tc.epsagon.com:443/traces'
21
+
19
22
  @@epsagon_config = {
20
- metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
21
- debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
22
- token: ENV['EPSAGON_TOKEN'],
23
- app_name: ENV['EPSAGON_APP_NAME'],
24
- backend: ENV['EPSAGON_BACKEND'] || 'localhost:55681/v1/trace'
25
- }
23
+ metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
24
+ debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
25
+ token: ENV['EPSAGON_TOKEN'],
26
+ app_name: ENV['EPSAGON_APP_NAME'],
27
+ backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND
28
+ }
26
29
 
27
30
  module_function
28
31
 
@@ -41,14 +44,15 @@ module Epsagon
41
44
  configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: @@epsagon_config }
42
45
  configurator.use 'EpsagonFaradayInstrumentation', { epsagon: @@epsagon_config }
43
46
  configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: @@epsagon_config }
47
+ configurator.use 'EpsagonRailsInstrumentation', { epsagon: @@epsagon_config }
44
48
 
45
49
  if @@epsagon_config[:debug]
46
50
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
47
51
  OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
48
- 'x-epsagon-token' => @@epsagon_config[:token]
49
- },
50
- endpoint: @@epsagon_config[:backend],
51
- insecure: @@epsagon_config[:insecure] || false)
52
+ 'x-epsagon-token' => @@epsagon_config[:token]
53
+ },
54
+ endpoint: @@epsagon_config[:backend],
55
+ insecure: @@epsagon_config[:insecure] || false)
52
56
  )
53
57
 
54
58
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
@@ -64,7 +68,6 @@ module Epsagon
64
68
  )
65
69
  end
66
70
  end
67
-
68
71
  end
69
72
 
70
73
  # monkey patch to include epsagon confs
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'aws-sdk-core'
2
4
  require 'opentelemetry/common'
3
5
  require 'opentelemetry/sdk'
4
6
 
5
- def untraced
6
- OpenTelemetry::Trace.with_span(OpenTelemetry::Trace::Span.new) { yield }
7
+ def untraced(&block)
8
+ OpenTelemetry::Trace.with_span(OpenTelemetry::Trace::Span.new, &block)
7
9
  end
10
+
8
11
  # AWS SDK plugin for epsagon instrumentation
9
12
  class EpsagonAwsPlugin < Seahorse::Client::Plugin
10
13
  def add_handlers(handlers, _)
@@ -15,13 +18,13 @@ end
15
18
  # Generates Spans for all uses of AWS SDK
16
19
  class EpsagonAwsHandler < Seahorse::Client::Handler
17
20
  def call(context)
18
- tracer.in_span('') do |span|
21
+ tracer.in_span('', kind: :client) do |span|
19
22
  untraced do
20
23
  @handler.call(context).tap do
21
24
  span.set_attribute('aws.service', context.client.class.to_s.split('::')[1].downcase)
22
- span.set_attribute('aws.aws.operation', context.operation.name)
25
+ span.set_attribute('aws.operation', context.operation.name)
23
26
  span.set_attribute('aws.region', context.client.config.region)
24
- span.set_attribute('aws.status_code', context.http_response.status_code)
27
+ span.set_attribute('http.status_code', context.http_response.status_code)
25
28
  end
26
29
  end
27
30
  end
@@ -30,4 +33,4 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
30
33
  def tracer
31
34
  EpsagonAwsSdkInstrumentation.instance.tracer
32
35
  end
33
- end
36
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require_relative '../util'
3
4
  require 'faraday'
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opentelemetry/trace/status'
4
+
5
+ module QueueTime
6
+ REQUEST_START = 'HTTP_X_REQUEST_START'
7
+ QUEUE_START = 'HTTP_X_QUEUE_START'
8
+ MINIMUM_ACCEPTABLE_TIME_VALUE = 1_000_000_000
9
+
10
+ module_function
11
+
12
+ def get_request_start(env, now = nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
13
+ header = env[REQUEST_START] || env[QUEUE_START]
14
+ return unless header
15
+
16
+ # nginx header is seconds in the format "t=1512379167.574"
17
+ # apache header is microseconds in the format "t=1570633834463123"
18
+ # heroku header is milliseconds in the format "1570634024294"
19
+ time_string = header.to_s.delete('^0-9')
20
+ return if time_string.nil?
21
+
22
+ # Return nil if the time is clearly invalid
23
+ time_value = "#{time_string[0, 10]}.#{time_string[10, 6]}".to_f
24
+ return if time_value.zero? || time_value < MINIMUM_ACCEPTABLE_TIME_VALUE
25
+
26
+ # return the request_start only if it's lesser than
27
+ # current time, to avoid significant clock skew
28
+ request_start = Time.at(time_value)
29
+ now ||= Time.now.utc
30
+ request_start.utc > now ? nil : request_start
31
+ rescue StandardError => e
32
+ # in case of an Exception we don't create a
33
+ # `request.queuing` span
34
+ OpenTelemetry.logger.debug("[rack] unable to parse request queue headers: #{e}")
35
+ nil
36
+ end
37
+ end
38
+
39
+ class EpsagonRackMiddleware
40
+ class << self
41
+ def allowed_rack_request_headers
42
+ @allowed_rack_request_headers ||= Array(config[:allowed_request_headers]).each_with_object({}) do |header, memo|
43
+ memo["HTTP_#{header.to_s.upcase.gsub(/[-\s]/, '_')}"] = build_attribute_name('http.request.headers.', header)
44
+ end
45
+ end
46
+
47
+ def allowed_response_headers
48
+ @allowed_response_headers ||= Array(config[:allowed_response_headers]).each_with_object({}) do |header, memo|
49
+ memo[header] = build_attribute_name('http.response.headers.', header)
50
+ memo[header.to_s.upcase] = build_attribute_name('http.response.headers.', header)
51
+ end
52
+ end
53
+
54
+ def build_attribute_name(prefix, suffix)
55
+ prefix + suffix.to_s.downcase.gsub(/[-\s]/, '_')
56
+ end
57
+
58
+ def config
59
+ EpsagonRailsInstrumentation.instance.config
60
+ end
61
+
62
+ private
63
+
64
+ def clear_cached_config
65
+ @allowed_rack_request_headers = nil
66
+ @allowed_response_headers = nil
67
+ end
68
+ end
69
+
70
+ EMPTY_HASH = {}.freeze
71
+
72
+ def initialize(app)
73
+ @app = app
74
+ end
75
+
76
+ def call(env)
77
+ original_env = env.dup
78
+ extracted_context = OpenTelemetry.propagation.http.extract(env)
79
+ frontend_context = create_frontend_span(env, extracted_context)
80
+
81
+ # restore extracted context in this process:
82
+ OpenTelemetry::Context.with_current(frontend_context || extracted_context) do
83
+ request_span_name = create_request_span_name(env['REQUEST_URI'] || original_env['PATH_INFO'])
84
+ tracer.in_span(env['HTTP_HOST'] || 'unknown',
85
+ attributes: request_span_attributes(env: env),
86
+ kind: :server) do |http_span|
87
+ RackExtension.with_span(http_span) do
88
+ tracer.in_span(
89
+ env['HTTP_HOST'],
90
+ kind: :server,
91
+ attributes: {type: 'rails'}
92
+ ) do |framework_span|
93
+ @app.call(env).tap do |status, headers, response|
94
+ set_attributes_after_request(http_span, framework_span, status, headers, response)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ ensure
101
+ finish_span(frontend_context)
102
+ end
103
+
104
+ private
105
+
106
+ # return Context with the frontend span as the current span
107
+ def create_frontend_span(env, extracted_context)
108
+ request_start_time = QueueTime.get_request_start(env)
109
+
110
+ return unless config[:record_frontend_span] && !request_start_time.nil?
111
+
112
+ span = tracer.start_span('http_server.proxy',
113
+ with_parent: extracted_context,
114
+ attributes: {
115
+ 'start_time' => request_start_time.to_f
116
+ },
117
+ kind: :server)
118
+
119
+ OpenTelemetry::Trace.context_with_span(span, parent_context: extracted_context)
120
+ end
121
+
122
+ def finish_span(context)
123
+ OpenTelemetry::Trace.current_span(context).finish if context
124
+ end
125
+
126
+ def tracer
127
+ EpsagonRailsInstrumentation.instance.tracer
128
+ end
129
+
130
+ def request_span_attributes(env:)
131
+ request = Rack::Request.new(env)
132
+ path, path_params = request.path.split(';')
133
+ request_headers = JSON.generate(Hash[*env.select { |k, _v| k.start_with? 'HTTP_' }
134
+ .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
135
+ .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
136
+ .sort
137
+ .flatten])
138
+
139
+ attributes = {
140
+ 'operation' => env['REQUEST_METHOD'],
141
+ 'type' => 'http',
142
+ 'http.scheme' => env['rack.url_scheme'],
143
+ 'http.request.path' => path,
144
+ 'http.request.headers' => request_headers
145
+ }
146
+
147
+ unless config[:epsagon][:metadata_only]
148
+ request.body.rewind
149
+ request_body = request.body.read
150
+ request.body.rewind
151
+
152
+ attributes.merge!(Util.epsagon_query_attributes(request.query_string))
153
+
154
+ attributes.merge!({
155
+ 'http.request.body' => request_body,
156
+ 'http.request.path_params' => path_params,
157
+ 'http.request.headers.User-Agent' => env['HTTP_USER_AGENT']
158
+ })
159
+ end
160
+
161
+ attributes
162
+ end
163
+
164
+ # e.g., "/webshop/articles/4?s=1":
165
+ def fullpath(env)
166
+ query_string = env['QUERY_STRING']
167
+ path = env['SCRIPT_NAME'] + env['PATH_INFO']
168
+
169
+ query_string.empty? ? path : "#{path}?#{query_string}"
170
+ end
171
+
172
+ # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#name
173
+ #
174
+ # recommendation: span.name(s) should be low-cardinality (e.g.,
175
+ # strip off query param value, keep param name)
176
+ #
177
+ # see http://github.com/open-telemetry/opentelemetry-specification/pull/416/files
178
+ def create_request_span_name(request_uri_or_path_info)
179
+ # NOTE: dd-trace-rb has implemented 'quantization' (which lowers url cardinality)
180
+ # see Datadog::Quantization::HTTP.url
181
+
182
+ if (implementation = config[:url_quantization])
183
+ implementation.call(request_uri_or_path_info)
184
+ else
185
+ request_uri_or_path_info
186
+ end
187
+ end
188
+
189
+ def set_attributes_after_request(http_span, _framework_span, status, headers, response)
190
+ unless config[:epsagon][:metadata_only]
191
+ http_span.set_attribute('http.response.headers', JSON.generate(headers))
192
+ http_span.set_attribute('http.response.body', response.join)
193
+ end
194
+
195
+ http_span.set_attribute('http.status_code', status)
196
+ http_span.status = OpenTelemetry::Trace::Status.http_to_status(status)
197
+ end
198
+
199
+ def allowed_request_headers(env)
200
+ return EMPTY_HASH if self.class.allowed_rack_request_headers.empty?
201
+
202
+ {}.tap do |result|
203
+ self.class.allowed_rack_request_headers.each do |key, value|
204
+ result[value] = env[key] if env.key?(key)
205
+ end
206
+ end
207
+ end
208
+
209
+ def allowed_response_headers(headers)
210
+ return EMPTY_HASH if headers.nil?
211
+ return EMPTY_HASH if self.class.allowed_response_headers.empty?
212
+
213
+ {}.tap do |result|
214
+ self.class.allowed_response_headers.each do |key, value|
215
+ if headers.key?(key)
216
+ result[value] = headers[key]
217
+ else
218
+ # do case-insensitive match:
219
+ headers.each do |k, v|
220
+ if k.upcase == key
221
+ result[value] = v
222
+ break
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ def config
231
+ EpsagonRailsInstrumentation.instance.config
232
+ end
233
+ end
234
+
235
+ # class EpsagonRackInstrumentation < OpenTelemetry::Instrumentation::Base
236
+ # install do |config|
237
+ # require_dependencies
238
+
239
+ # retain_middleware_names if config[:retain_middleware_names]
240
+ # end
241
+
242
+ # present do
243
+ # defined?(::Rack)
244
+ # end
245
+
246
+ # private
247
+
248
+ # def require_dependencies
249
+ # require_relative 'middlewares/tracer_middleware'
250
+ # end
251
+
252
+ # MissingApplicationError = Class.new(StandardError)
253
+
254
+ # # intercept all middleware-compatible calls, retain class name
255
+ # def retain_middleware_names
256
+ # next_middleware = config[:application]
257
+ # raise MissingApplicationError unless next_middleware
258
+
259
+ # while next_middleware
260
+ # if next_middleware.respond_to?(:call)
261
+ # next_middleware.singleton_class.class_eval do
262
+ # alias_method :__call, :call
263
+
264
+ # def call(env)
265
+ # env['RESPONSE_MIDDLEWARE'] = self.class.to_s
266
+ # __call(env)
267
+ # end
268
+ # end
269
+ # end
270
+
271
+ # next_middleware = next_middleware.instance_variable_defined?('@app') &&
272
+ # next_middleware.instance_variable_get('@app')
273
+ # end
274
+ # end
275
+ # end
276
+
277
+ class EpsagonRailtie < ::Rails::Railtie
278
+ config.before_initialize do |app|
279
+ # EpsagonRackInstrumentation.instance.install({})
280
+
281
+ app.middleware.insert_after(
282
+ ActionDispatch::RequestId,
283
+ EpsagonRackMiddleware
284
+ )
285
+ end
286
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # Patch faraday to include middleware
3
4
  module EpsagonFaradayPatch
4
5
  def adapter(*args)
@@ -40,7 +40,7 @@ module EpsagonNetHTTPExtension
40
40
  tracer.in_span(
41
41
  @address,
42
42
  attributes: attributes,
43
- kind: :Client
43
+ kind: :client
44
44
  ) do |span|
45
45
  OpenTelemetry.propagation.http.inject(req)
46
46
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opentelemetry'
4
+ require 'rails'
5
+ require 'action_controller/railtie'
6
+
7
+ require_relative '../util'
8
+
9
+ module RackExtension
10
+ module_function
11
+
12
+ CURRENT_SPAN_KEY = OpenTelemetry::Context.create_key('current-span')
13
+
14
+ private_constant :CURRENT_SPAN_KEY
15
+
16
+ # Returns the current span from the current or provided context
17
+ #
18
+ # @param [optional Context] context The context to lookup the current
19
+ # {Span} from. Defaults to Context.current
20
+ def current_span(context = nil)
21
+ context ||= OpenTelemetry::Context.current
22
+ context.value(CURRENT_SPAN_KEY) || OpenTelemetry::Trace::Span::INVALID
23
+ end
24
+
25
+ # Returns a context containing the span, derived from the optional parent
26
+ # context, or the current context if one was not provided.
27
+ #
28
+ # @param [optional Context] context The context to use as the parent for
29
+ # the returned context
30
+ def context_with_span(span, parent_context: OpenTelemetry::Context.current)
31
+ parent_context.set_value(CURRENT_SPAN_KEY, span)
32
+ end
33
+
34
+ # Activates/deactivates the Span within the current Context, which makes the "current span"
35
+ # available implicitly.
36
+ #
37
+ # On exit, the Span that was active before calling this method will be reactivated.
38
+ #
39
+ # @param [Span] span the span to activate
40
+ # @yield [span, context] yields span and a context containing the span to the block.
41
+ def with_span(span)
42
+ OpenTelemetry::Context.with_value(CURRENT_SPAN_KEY, span) { |c, s| yield s, c }
43
+ end
44
+ end
45
+
46
+ module MetalPatch
47
+ def dispatch(name, request, response)
48
+ rack_span = RackExtension.current_span
49
+ # rack_span.name = "#{self.class.name}##{name}" if rack_span.context.valid? && !request.env['action_dispatch.exception']
50
+ super(name, request, response)
51
+ end
52
+ end
53
+
54
+ class EpsagonRailsInstrumentation < OpenTelemetry::Instrumentation::Base
55
+ install do |_config|
56
+ require_relative 'epsagon_rails_middleware'
57
+ ::ActionController::Metal.prepend(MetalPatch)
58
+ end
59
+
60
+ present do
61
+ defined?(::Rails)
62
+ end
63
+ end
@@ -26,7 +26,7 @@ class EpsagonTracerMiddleware
26
26
  attributes = {
27
27
  'operation' => env['REQUEST_METHOD'],
28
28
  'type' => 'http',
29
- 'http.scheme' => env['PATH_INFO'],
29
+ 'http.scheme' => env['rack.url_scheme'],
30
30
  'http.request.path' => path,
31
31
  'http.request.headers' => request_headers
32
32
  }
@@ -51,7 +51,11 @@ class EpsagonTracerMiddleware
51
51
  kind: :server,
52
52
  with_parent: parent_context(env)
53
53
  ) do |http_span|
54
- tracer.in_span('sinatra') do |framework_span|
54
+ tracer.in_span(
55
+ env['HTTP_HOST'],
56
+ kind: :server,
57
+ attributes: { type: 'sinatra' }
58
+ ) do |framework_span|
55
59
  app.call(env).tap { |resp| trace_response(http_span, framework_span, env, resp) }
56
60
  end
57
61
  end
@@ -78,7 +82,7 @@ class EpsagonTracerMiddleware
78
82
  end
79
83
 
80
84
  http_span.set_attribute('http.status_code', status)
81
- framework_span.set_attribute('http.route', env['sinatra.route'].split.last) if env['sinatra.route']
85
+ http_span.set_attribute('http.route', env['sinatra.route'].split.last) if env['sinatra.route']
82
86
  http_span.status = OpenTelemetry::Trace::Status.http_to_status(status)
83
87
  end
84
88
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: epsagon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Epsagon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-12 00:00:00.000000000 Z
11
+ date: 2021-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-api
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.11.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: opentelemetry-sdk
28
+ name: opentelemetry-exporter-otlp
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.11.1
33
+ version: 0.11.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.11.1
40
+ version: 0.11.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: opentelemetry-instrumentation-sinatra
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,19 +53,19 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.11.0
55
55
  - !ruby/object:Gem::Dependency
56
- name: opentelemetry-exporter-otlp
56
+ name: opentelemetry-sdk
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.11.0
61
+ version: 0.11.1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.11.0
68
+ version: 0.11.1
69
69
  description: 'Epsagon provides tracing to Ruby applications for the collection of
70
70
  distributed tracing and performance metrics to simplify complex architectures, eliminate
71
71
  manual work, visualize and correlate data to identify and fix problems fast.
@@ -80,8 +80,10 @@ files:
80
80
  - lib/instrumentation/aws_sdk.rb
81
81
  - lib/instrumentation/aws_sdk_plugin.rb
82
82
  - lib/instrumentation/epsagon_faraday_middleware.rb
83
+ - lib/instrumentation/epsagon_rails_middleware.rb
83
84
  - lib/instrumentation/faraday.rb
84
85
  - lib/instrumentation/net_http.rb
86
+ - lib/instrumentation/rails.rb
85
87
  - lib/instrumentation/sinatra.rb
86
88
  - lib/util.rb
87
89
  homepage: https://github.com/epsagon/epsagon-ruby