epsagon 0.0.11 → 0.0.12

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