epsagon 0.0.11 → 0.0.12

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