epsagon 0.0.26 → 0.0.30

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: 8c6a213fb12b5ded33f1d938b306f56f6de61936197c10be80d53b11258483c1
4
- data.tar.gz: 34890a4a25d8bf528dc442b975504d1e70b177a09a7868b56538a9b6b112f3d0
3
+ metadata.gz: f8ebb5eb2134eb62b3b83fa8cbb48b9c535cf528999c55ad1b58ed161b63cd97
4
+ data.tar.gz: 1e5ac5ea5ef2d6a3ba156b89061cca64bab22b90ce27b37e128a9b62c2633526
5
5
  SHA512:
6
- metadata.gz: b0ba4c84e5db6f25b6f68c7c31cbd8c694098ac8542c893735955328a18b58f5b49b436dab779dcac6759f246394c8152e0c3ef8791e4169a4abcf2f73e831e0
7
- data.tar.gz: 95f6e44d44be2917c1a6cb9b224c838a1b46aa0866fe991c5aa8e0ebf6cbd9703f71ede6d871c7eaebbd499d391c19bd8d887ee25a637c581628362e2fa7442d
6
+ metadata.gz: 34adf61a4771b2e61f916a79f3e038a1b23235214d13ec9771c33e3a1be34edc843c735f2ca2fd6d1a84fc895e9ffd7f6ec4b7674b1ace141b39f193e0d7b7b0
7
+ data.tar.gz: 289999c6c4d93c3521598fc0ac30eefd29f83e46f74079d1133b50d4a459c6f8e397f6fa1cf151492308d6089b9bfda9f9ed4279aab29c0e1de787aba26e4361
data/lib/arn_parser.rb ADDED
@@ -0,0 +1,27 @@
1
+ #
2
+ # Credit: https://gist.github.com/RulerOf/b9f5dd00a9911aba8271b57d3d269d7a
3
+ #
4
+ class Arn
5
+ attr_accessor :partition, :service, :region, :account, :resource
6
+
7
+ def initialize(partition, service, region, account, resource)
8
+ @partition = partition
9
+ @service = service
10
+ @region = region
11
+ @account = account
12
+ @resource = resource
13
+ end
14
+
15
+ def self.parse(arn)
16
+ raise TypeError, 'ARN must be supplied as a string' unless arn.is_a?(String)
17
+
18
+ arn_components = arn.split(':', 6)
19
+ raise ArgumentError, 'Could not parse ARN' if arn_components.length < 6
20
+
21
+ Arn.new arn_components[1],
22
+ arn_components[2],
23
+ arn_components[3],
24
+ arn_components[4],
25
+ arn_components[5]
26
+ end
27
+ end
data/lib/epsagon.rb CHANGED
@@ -12,77 +12,129 @@ require_relative 'instrumentation/net_http'
12
12
  require_relative 'instrumentation/faraday'
13
13
  require_relative 'instrumentation/aws_sdk'
14
14
  require_relative 'instrumentation/rails'
15
+ require_relative 'instrumentation/postgres'
16
+ require_relative 'instrumentation/resque'
15
17
  require_relative 'util'
16
18
  require_relative 'epsagon_constants'
17
19
  require_relative 'exporter_extension'
20
+ require_relative 'arn_parser'
18
21
 
19
22
  Bundler.require
20
23
 
24
+
21
25
  # Epsagon tracing main entry point
22
26
  module Epsagon
23
27
  DEFAULT_BACKEND = 'opentelemetry.tc.epsagon.com:443/traces'
24
- DEFAULT_IGNORE_DOMAINS = ['newrelic.com']
28
+ DEFAULT_IGNORE_DOMAINS = ['newrelic.com'].freeze
29
+ MUTABLE_CONF_KEYS = Set.new([:metadata_only, :max_attribute_size, :ignore_domains, :ignored_keys])
25
30
 
26
- @@epsagon_config = {}
31
+ @@epsagon_config = nil
27
32
 
28
33
  module_function
29
34
 
30
35
  def init(**args)
31
- @@epsagon_config = {
36
+ get_config.merge!(args)
37
+ validate(get_config)
38
+ OpenTelemetry::SDK.configure
39
+ @@initialized = true
40
+ end
41
+
42
+ def validate(config)
43
+ Util.validate_value(config, :metadata_only, 'Must be a boolean') {|v| !!v == v}
44
+ Util.validate_value(config, :debug, 'Must be a boolean') {|v| !!v == v}
45
+ Util.validate_value(config, :token, 'Must be a valid Epsagon token') {|v| (v.is_a? String) && (v.size > 10) }
46
+ Util.validate_value(config, :app_name, 'Must be a String') {|v| (v.is_a? String) && (v.size > 0) }
47
+ Util.validate_value(config, :max_attribute_size, 'Must be an Integer') {|v| v.is_a? Integer}
48
+ Util.validate_value(config, :ignore_domains, 'Must be iterable') {|v| v.respond_to?(:each)}
49
+ Util.validate_value(config, :ignored_keys, 'Must be iterable') {|v| v.respond_to?(:each)}
50
+ end
51
+
52
+ def set_config(**args)
53
+ unless args.keys.all? {|a| MUTABLE_CONF_KEYS.include?(a)}
54
+ raise ArgumentError.new("only #{MUTABLE_CONF_KEYS.to_a} are mutable after `Epsagon.init`")
55
+ end
56
+ Epsagon.init unless @@initialized
57
+ new_conf = get_config.merge(args)
58
+ validate(new_conf)
59
+ @@epsagon_config = new_conf
60
+ end
61
+
62
+ def add_ignored_key(key)
63
+ get_config[:ignored_keys].push(key).uniq
64
+ end
65
+
66
+ def remove_ignored_key(key)
67
+ get_config[:ignored_keys].delete(key)
68
+ end
69
+
70
+ def get_config
71
+ @@epsagon_config ||= {
32
72
  metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
33
73
  debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
34
74
  token: ENV['EPSAGON_TOKEN'] || '',
35
75
  app_name: ENV['EPSAGON_APP_NAME'] || '',
36
76
  max_attribute_size: ENV['EPSAGON_MAX_ATTRIBUTE_SIZE'] || 5000,
37
77
  backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND,
38
- ignore_domains: ENV['EPSAGON_IGNORE_DOMAINS'] || DEFAULT_IGNORE_DOMAINS
78
+ ignore_domains: ENV['EPSAGON_IGNORE_DOMAINS']&.split(',') || DEFAULT_IGNORE_DOMAINS,
79
+ ignored_keys: ENV['EPSAGON_IGNORED_KEYS']&.split(',') || []
39
80
  }
40
- @@epsagon_config.merge!(args)
41
- OpenTelemetry::SDK.configure
42
81
  end
43
82
 
44
- def get_config
45
- @@epsagon_config
83
+ def set_ecs_metadata
84
+ metadata_uri = ENV['ECS_CONTAINER_METADATA_URI']
85
+ return {} if metadata_uri.nil?
86
+
87
+ response = Net::HTTP.get(URI(metadata_uri))
88
+ ecs_metadata = JSON.parse(response)
89
+ arn = Arn.parse(ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'])
90
+
91
+ {
92
+ 'aws.account_id' => arn.account,
93
+ 'aws.region' => arn.region,
94
+ 'aws.ecs.cluster' => ecs_metadata['Labels']['com.amazonaws.ecs.cluster'],
95
+ 'aws.ecs.task_arn' => ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'],
96
+ 'aws.ecs.container_name' => ecs_metadata['Labels']['com.amazonaws.ecs.container-name'],
97
+ 'aws.ecs.task.family' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-family'],
98
+ 'aws.ecs.task.revision' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-version']
99
+ }
46
100
  end
47
101
 
48
102
  # config opentelemetry with epsaon extensions:
49
103
 
50
104
  def epsagon_confs(configurator)
105
+ otel_resource = {
106
+ 'application' => get_config[:app_name],
107
+ 'epsagon.version' => EpsagonConstants::VERSION,
108
+ 'epsagon.metadata_only' => get_config[:metadata_only]
109
+ }.merge(set_ecs_metadata)
110
+
51
111
  configurator.resource = OpenTelemetry::SDK::Resources::Resource.telemetry_sdk.merge(
52
- OpenTelemetry::SDK::Resources::Resource.create({
53
- 'application' => @@epsagon_config[:app_name],
54
- 'epsagon.version' => EpsagonConstants::VERSION,
55
- 'epsagon.metadata_only' => @@epsagon_config[:metadata_only]
56
- })
112
+ OpenTelemetry::SDK::Resources::Resource.create(otel_resource)
57
113
  )
58
- configurator.use 'EpsagonSinatraInstrumentation', { epsagon: @@epsagon_config }
59
- configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: @@epsagon_config }
60
- configurator.use 'EpsagonFaradayInstrumentation', { epsagon: @@epsagon_config }
61
- configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: @@epsagon_config }
62
- configurator.use 'EpsagonRailsInstrumentation', { epsagon: @@epsagon_config }
63
- configurator.use 'OpenTelemetry::Instrumentation::Sidekiq', { epsagon: @@epsagon_config }
64
-
65
- if @@epsagon_config[:debug]
66
- configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
67
- OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
68
- 'x-epsagon-token' => @@epsagon_config[:token]
69
- },
70
- endpoint: @@epsagon_config[:backend],
71
- insecure: @@epsagon_config[:insecure] || false)
72
- )
73
114
 
115
+ configurator.use 'EpsagonSinatraInstrumentation', { epsagon: get_config }
116
+ configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: get_config }
117
+ configurator.use 'EpsagonFaradayInstrumentation', { epsagon: get_config }
118
+ configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: get_config }
119
+ configurator.use 'EpsagonRailsInstrumentation', { epsagon: get_config }
120
+ configurator.use 'OpenTelemetry::Instrumentation::Sidekiq', { epsagon: get_config }
121
+ configurator.use 'EpsagonPostgresInstrumentation', { epsagon: get_config }
122
+ configurator.use 'EpsagonResqueInstrumentation', { epsagon: get_config }
123
+
124
+
125
+ if get_config[:debug]
74
126
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
75
127
  OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new
76
128
  )
77
- else
78
- configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
79
- exporter: OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
80
- 'x-epsagon-token' => @@epsagon_config[:token]
81
- },
82
- endpoint: @@epsagon_config[:backend],
83
- insecure: @@epsagon_config[:insecure] || false)
84
- )
85
129
  end
130
+
131
+ configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
132
+ exporter: OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
133
+ 'x-epsagon-token' => get_config[:token]
134
+ },
135
+ endpoint: get_config[:backend],
136
+ insecure: get_config[:insecure] || false)
137
+ )
86
138
  end
87
139
  end
88
140
 
@@ -91,10 +143,15 @@ module SpanExtension
91
143
 
92
144
  BLANKS = [nil, [], '']
93
145
 
146
+ def set_mapping_attribute(key, value)
147
+ value = Util.prepare_attr(key, value, Epsagon.get_config[:max_attribute_size], Epsagon.get_config[:ignored_keys])
148
+ set_attribute(key, value) if value
149
+ end
150
+
94
151
  def set_attribute(key, value)
95
152
  unless BLANKS.include?(value)
96
- value = Util.trim_attr(value, Epsagon.get_config[:max_attribute_size])
97
- super(key, value)
153
+ value = Util.prepare_attr(key, value, Epsagon.get_config[:max_attribute_size], Epsagon.get_config[:ignored_keys])
154
+ super(key, value) if value
98
155
  end
99
156
  end
100
157
 
@@ -102,8 +159,9 @@ module SpanExtension
102
159
  super(*args)
103
160
  if @attributes
104
161
  @attributes = Hash[@attributes.select {|k,v| not BLANKS.include? v}.map { |k,v|
105
- [k, Util.trim_attr(v, Epsagon.get_config[:max_attribute_size])]
106
- }]
162
+ v = Util.prepare_attr(k, v, Epsagon.get_config[:max_attribute_size], Epsagon.get_config[:ignored_keys])
163
+ [k, v] if v
164
+ }.compact]
107
165
  end
108
166
  end
109
167
  end
@@ -1,3 +1,3 @@
1
1
  module EpsagonConstants
2
- VERSION = '0.0.26'
2
+ VERSION = '0.0.30'
3
3
  end
@@ -7,6 +7,7 @@ require_relative '../epsagon_constants'
7
7
  class EpsagonAwsSdkInstrumentation < OpenTelemetry::Instrumentation::Base
8
8
  VERSION = EpsagonConstants::VERSION
9
9
  SERVICES = %w[
10
+ SecretsManager
10
11
  ACM
11
12
  APIGateway
12
13
  AppStream
@@ -59,10 +59,10 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
59
59
  'message_body' => m[:message_body],
60
60
  }
61
61
  end
62
- attributes['aws.sqs.record'] = JSON.dump(messages_attributes) if messages_attributes
62
+ attributes['aws.sqs.record'] = messages_attributes if messages_attributes
63
63
  end
64
64
  attributes['aws.sqs.record.message_body'] = context.params[:message_body]
65
- attributes['aws.sqs.record.message_attributes'] = JSON.dump(context.params[:message_attributes]) if context.params[:message_attributes]
65
+ attributes['aws.sqs.record.message_attributes'] = context.params[:message_attributes] if context.params[:message_attributes]
66
66
  end
67
67
  elsif attributes['aws.service'] == 'sns'
68
68
  topic_arn = context.params[:topic_arn]
@@ -71,7 +71,7 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
71
71
  unless config[:epsagon][:metadata_only]
72
72
  attributes['aws.sns.subject'] = context.params[:subject]
73
73
  attributes['aws.sns.message'] = context.params[:message]
74
- attributes['aws.sns.message_attributes'] = JSON.dump(context.params[:message_attributes]) if context.params[:message_attributes]
74
+ attributes['aws.sns.message_attributes'] = context.params[:message_attributes] if context.params[:message_attributes]
75
75
  end
76
76
  end
77
77
  tracer.in_span(span_name, kind: span_kind, attributes: attributes) do |span|
@@ -105,7 +105,7 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
105
105
  end
106
106
  record
107
107
  end
108
- span.set_attribute('aws.sqs.record', JSON.dump(messages_attributes)) if messages_attributes
108
+ span.set_mapping_attribute('aws.sqs.record', messages_attributes) if messages_attributes
109
109
  end
110
110
  if context.operation.name == 'ReceiveMessage'
111
111
  messages_attributes = result.messages.map do |m|
@@ -123,7 +123,7 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
123
123
  end
124
124
  record
125
125
  end
126
- span.set_attribute('aws.sqs.record', JSON.dump(messages_attributes)) if messages_attributes
126
+ span.set_mapping_attribute('aws.sqs.record', messages_attributes) if messages_attributes
127
127
  end
128
128
  elsif attributes['aws.service'] == 'sns'
129
129
  span.set_attribute('aws.sns.message_id', result.message_id) if context.operation.name == 'Publish'
@@ -38,7 +38,7 @@ class EpsagonFaradayMiddleware < ::Faraday::Middleware
38
38
  attributes.merge!(Util.epsagon_query_attributes(env.url.query))
39
39
  attributes.merge!({
40
40
  'http.request.path_params' => path_params,
41
- 'http.request.headers' => env.request_headers.to_json,
41
+ 'http.request.headers' => Hash[env.request_headers],
42
42
  'http.request.body' => env.body,
43
43
  'http.request.headers.User-Agent' => env.request_headers['User-Agent']
44
44
  })
@@ -67,7 +67,7 @@ class EpsagonFaradayMiddleware < ::Faraday::Middleware
67
67
  span.set_attribute('http.status_code', response.status)
68
68
 
69
69
  unless config[:epsagon][:metadata_only]
70
- span.set_attribute('http.response.headers', response.headers.to_json)
70
+ span.set_attribute('http.response.headers', Hash[env.response.headers])
71
71
  span.set_attribute('http.response.body', response.body)
72
72
  end
73
73
  span.status = OpenTelemetry::Trace::Status.http_to_status(
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'opentelemetry/trace/status'
4
+ require 'action_controller/railtie'
4
5
 
5
6
  module QueueTime
6
7
  REQUEST_START = 'HTTP_X_REQUEST_START'
@@ -189,7 +190,7 @@ class EpsagonRackMiddleware
189
190
  def set_attributes_after_request(http_span, _framework_span, status, headers, response)
190
191
  unless config[:epsagon][:metadata_only]
191
192
  http_span.set_attribute('http.response.headers', JSON.generate(headers))
192
- http_span.set_attribute('http.response.body', response.join)
193
+ http_span.set_attribute('http.response.body', response.join) if response.respond_to?(:join)
193
194
  end
194
195
 
195
196
  http_span.set_attribute('http.status_code', status)
@@ -284,3 +285,48 @@ class EpsagonRailtie < ::Rails::Railtie
284
285
  )
285
286
  end
286
287
  end
288
+
289
+ module RackExtension
290
+ module_function
291
+
292
+ CURRENT_SPAN_KEY = OpenTelemetry::Context.create_key('current-span')
293
+
294
+ private_constant :CURRENT_SPAN_KEY
295
+
296
+ # Returns the current span from the current or provided context
297
+ #
298
+ # @param [optional Context] context The context to lookup the current
299
+ # {Span} from. Defaults to Context.current
300
+ def current_span(context = nil)
301
+ context ||= OpenTelemetry::Context.current
302
+ context.value(CURRENT_SPAN_KEY) || OpenTelemetry::Trace::Span::INVALID
303
+ end
304
+
305
+ # Returns a context containing the span, derived from the optional parent
306
+ # context, or the current context if one was not provided.
307
+ #
308
+ # @param [optional Context] context The context to use as the parent for
309
+ # the returned context
310
+ def context_with_span(span, parent_context: OpenTelemetry::Context.current)
311
+ parent_context.set_value(CURRENT_SPAN_KEY, span)
312
+ end
313
+
314
+ # Activates/deactivates the Span within the current Context, which makes the "current span"
315
+ # available implicitly.
316
+ #
317
+ # On exit, the Span that was active before calling this method will be reactivated.
318
+ #
319
+ # @param [Span] span the span to activate
320
+ # @yield [span, context] yields span and a context containing the span to the block.
321
+ def with_span(span)
322
+ OpenTelemetry::Context.with_value(CURRENT_SPAN_KEY, span) { |c, s| yield s, c }
323
+ end
324
+ end
325
+
326
+ module MetalPatch
327
+ def dispatch(name, request, response)
328
+ rack_span = RackExtension.current_span
329
+ # rack_span.name = "#{self.class.name}##{name}" if rack_span.context.valid? && !request.env['action_dispatch.exception']
330
+ super(name, request, response)
331
+ end
332
+ end
@@ -0,0 +1,113 @@
1
+ require 'json'
2
+
3
+ module EpsagonResqueModule
4
+ def self.prepended(base)
5
+ class << base
6
+ prepend ClassMethods
7
+ end
8
+ end
9
+
10
+ # Module to prepend to Resque singleton class
11
+ module ClassMethods
12
+ def push(queue, item)
13
+ epsagon_conf = config[:epsagon] || {}
14
+ # Check if the job is being wrapped by ActiveJob
15
+ # before retrieving the job class name
16
+ job_class = if item[:class] == 'ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper' && item[:args][0]&.is_a?(Hash)
17
+ item[:args][0]['job_class']
18
+ else
19
+ item[:class]
20
+ end
21
+ attributes = {
22
+ 'operation' => 'enqueue',
23
+ 'messaging.system' => 'resque',
24
+ 'messaging.resque.job_class' => job_class,
25
+ 'messaging.destination' => queue.to_s,
26
+ 'messaging.destination_kind' => 'queue',
27
+ 'messaging.resque.redis_url' => Resque.redis.connection[:id]
28
+ }
29
+ unless epsagon_conf[:metadata_only]
30
+ attributes.merge!({
31
+ 'messaging.resque.args' => item
32
+ })
33
+ end
34
+
35
+ tracer.in_span(queue.to_s, attributes: attributes, kind: :producer) do
36
+ OpenTelemetry.propagation.text.inject(item)
37
+ super
38
+ end
39
+ end
40
+
41
+ def tracer
42
+ EpsagonResqueInstrumentation.instance.tracer
43
+ end
44
+
45
+ def config
46
+ EpsagonResqueInstrumentation.instance.config
47
+ end
48
+ end
49
+ end
50
+
51
+ module EpsagonResqueJob
52
+ def perform # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
53
+ inner_exception = nil
54
+ epsagon_conf = config[:epsagon] || {}
55
+ job_args = args || []
56
+
57
+ # Check if the job is being wrapped by ActiveJob
58
+ # before retrieving the job class name
59
+ job_class = if payload_class_name == 'ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper' && job_args[0]&.is_a?(Hash)
60
+ job_args[0]['job_class']
61
+ else
62
+ payload_class_name
63
+ end
64
+
65
+ attributes = {
66
+ 'operation' => 'perform',
67
+ 'messaging.system' => 'resque',
68
+ 'messaging.resque.job_class' => job_class,
69
+ 'messaging.destination' => queue.to_s,
70
+ 'messaging.destination_kind' => 'queue',
71
+ 'messaging.resque.redis_url' => Resque.redis.connection[:id]
72
+ }
73
+ runner_attributes = {
74
+ 'type' => 'resque_worker',
75
+ 'messaging.resque.redis_url' => Resque.redis.connection[:id],
76
+
77
+ }
78
+
79
+ extracted_context = OpenTelemetry.propagation.text.extract(@payload)
80
+
81
+ unless epsagon_conf[:metadata_only]
82
+ attributes.merge!({
83
+ 'messaging.resque.args' => args
84
+ })
85
+ end
86
+ tracer.in_span(
87
+ queue.to_s,
88
+ attributes: attributes,
89
+ with_parent: extracted_context,
90
+ kind: :consumer
91
+ ) do |trigger_span|
92
+ tracer.in_span(job_class,
93
+ attributes: runner_attributes,
94
+ kind: :consumer
95
+ ) do |runner_span|
96
+ super
97
+ end
98
+ rescue Exception => e
99
+ inner_exception = e
100
+ end
101
+ raise inner_exception if inner_exception
102
+ end
103
+
104
+ private
105
+
106
+ def tracer
107
+ EpsagonResqueInstrumentation.instance.tracer
108
+ end
109
+
110
+ def config
111
+ EpsagonResqueInstrumentation.instance.config
112
+ end
113
+ end
@@ -34,7 +34,7 @@ module EpsagonNetHTTPExtension
34
34
  attributes.merge!({
35
35
  'http.request.path_params' => path_params,
36
36
  'http.request.body' => body,
37
- 'http.request.headers' => headers.to_json,
37
+ 'http.request.headers' => Hash[headers],
38
38
  'http.request.headers.User-Agent' => headers['user-agent']
39
39
  })
40
40
  attributes.merge!(Util.epsagon_query_attributes(query))
@@ -62,7 +62,7 @@ module EpsagonNetHTTPExtension
62
62
 
63
63
  span.set_attribute('http.status_code', status_code)
64
64
  unless config[:epsagon][:metadata_only]
65
- span.set_attribute('http.response.headers', Hash[response.each_header.to_a].to_json)
65
+ span.set_attribute('http.response.headers', Hash[response.each_header.to_a])
66
66
  span.set_attribute('http.response.body', response.body)
67
67
  end
68
68
  span.status = OpenTelemetry::Trace::Status.http_to_status(
@@ -0,0 +1,294 @@
1
+ require 'pg_query'
2
+
3
+ module PostgresExtension
4
+ # A list of SQL commands, from: https://www.postgresql.org/docs/current/sql-commands.html
5
+ # Commands are truncated to their first word, and all duplicates
6
+ # are removed, This favors brevity and low-cardinality over descriptiveness.
7
+ SQL_COMMANDS = %w[
8
+ ABORT
9
+ ALTER
10
+ ANALYZE
11
+ BEGIN
12
+ CALL
13
+ CHECKPOINT
14
+ CLOSE
15
+ CLUSTER
16
+ COMMENT
17
+ COMMIT
18
+ COPY
19
+ CREATE
20
+ DEALLOCATE
21
+ DECLARE
22
+ DELETE
23
+ DISCARD
24
+ DO
25
+ DROP
26
+ END
27
+ EXECUTE
28
+ EXPLAIN
29
+ FETCH
30
+ GRANT
31
+ IMPORT
32
+ INSERT
33
+ LISTEN
34
+ LOAD
35
+ LOCK
36
+ MOVE
37
+ NOTIFY
38
+ PREPARE
39
+ PREPARE
40
+ REASSIGN
41
+ REFRESH
42
+ REINDEX
43
+ RELEASE
44
+ RESET
45
+ REVOKE
46
+ ROLLBACK
47
+ SAVEPOINT
48
+ SECURITY
49
+ SELECT
50
+ SELECT
51
+ SET
52
+ SHOW
53
+ START
54
+ TRUNCATE
55
+ UNLISTEN
56
+ UPDATE
57
+ VACUUM
58
+ VALUES
59
+ ].freeze
60
+
61
+ # From: https://github.com/newrelic/newrelic-ruby-agent/blob/9787095d4b5b2d8fcaf2fdbd964ed07c731a8b6b/lib/new_relic/agent/database/obfuscation_helpers.rb#L9-L34
62
+ COMPONENTS_REGEX_MAP = {
63
+ single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
64
+ dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
65
+ uuids: /\{?(?:[0-9a-fA-F]\-*){32}\}?/,
66
+ numeric_literals: /-?\b(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
67
+ boolean_literals: /\b(?:true|false|null)\b/i,
68
+ comments: /(?:#|--).*?(?=\r|\n|$)/i,
69
+ multi_line_comments: %r{\/\*(?:[^\/]|\/[^*])*?(?:\*\/|\/\*.*)}
70
+ }.freeze
71
+
72
+ POSTGRES_COMPONENTS = %i[
73
+ single_quotes
74
+ dollar_quotes
75
+ uuids
76
+ numeric_literals
77
+ boolean_literals
78
+ comments
79
+ multi_line_comments
80
+ ].freeze
81
+
82
+ UNMATCHED_PAIRS_REGEX = %r{'|\/\*|\*\/|\$(?!\?)}.freeze
83
+
84
+ # These are all alike in that they will have a SQL statement as the first parameter.
85
+ # That statement may possibly be parameterized, but we can still use it - the
86
+ # obfuscation code will just transform $1 -> $? in that case (which is fine enough).
87
+ EXEC_ISH_METHODS = %i[
88
+ exec
89
+ query
90
+ sync_exec
91
+ async_exec
92
+ exec_params
93
+ async_exec_params
94
+ sync_exec_params
95
+ ].freeze
96
+
97
+ # The following methods all take a statement name as the first
98
+ # parameter, and a SQL statement as the second - and possibly
99
+ # further parameters after that. We can trace them all alike.
100
+ PREPARE_ISH_METHODS = %i[
101
+ prepare
102
+ async_prepare
103
+ sync_prepare
104
+ ].freeze
105
+
106
+ # The following methods take a prepared statement name as their first
107
+ # parameter - everything after that is either potentially quite sensitive
108
+ # (an array of bind params) or not useful to us. We trace them all alike.
109
+ EXEC_PREPARED_ISH_METHODS = %i[
110
+ exec_prepared
111
+ async_exec_prepared
112
+ sync_exec_prepared
113
+ ].freeze
114
+
115
+ EXEC_ISH_METHODS.each do |method|
116
+ define_method method do |*args|
117
+ span_name, attrs = span_attrs(:query, *args)
118
+ tracer.in_span(span_name, attributes: attrs, kind: :client) do
119
+ super(*args)
120
+ end
121
+ end
122
+ end
123
+
124
+ PREPARE_ISH_METHODS.each do |method|
125
+ define_method method do |*args|
126
+ span_name, attrs = span_attrs(:prepare, *args)
127
+ tracer.in_span(span_name, attributes: attrs, kind: :client) do
128
+ super(*args)
129
+ end
130
+ end
131
+ end
132
+
133
+ EXEC_PREPARED_ISH_METHODS.each do |method|
134
+ define_method method do |*args|
135
+ span_name, attrs = span_attrs(:execute, *args)
136
+ tracer.in_span(span_name, attributes: attrs, kind: :client) do
137
+ super(*args)
138
+ end
139
+ end
140
+ end
141
+
142
+ def config
143
+ EpsagonPostgresInstrumentation.instance.config
144
+ end
145
+
146
+ def tracer
147
+ EpsagonPostgresInstrumentation.instance.tracer
148
+ end
149
+
150
+ def lru_cache
151
+ # When SQL is being sanitized, we know that this cache will
152
+ # never be more than 50 entries * 2000 characters (so, presumably
153
+ # 100k bytes - or 97k). When not sanitizing SQL, then this cache
154
+ # could grow much larger - but the small cache size should otherwise
155
+ # help contain memory growth. The intended use here is to cache
156
+ # prepared SQL statements, so that we can attach a reasonable
157
+ # `db.sql.statement` value to spans when those prepared statements
158
+ # are executed later on.
159
+ @lru_cache ||= LruCache.new(50)
160
+ end
161
+
162
+ # Rubocop is complaining about 19.31/18 for Metrics/AbcSize.
163
+ # But, getting that metric in line would force us over the
164
+ # module size limit! We can't win here unless we want to start
165
+ # abstracting things into a million pieces.
166
+ def span_attrs(kind, *args) # rubocop:disable Metrics/AbcSize
167
+ if kind == :query
168
+ operation = extract_operation(args[0])
169
+ sql = args[0]
170
+ else
171
+ statement_name = args[0]
172
+
173
+ if kind == :prepare
174
+ sql = args[1]
175
+ lru_cache[statement_name] = sql
176
+ operation = 'PREPARE'
177
+ else
178
+ sql = lru_cache[statement_name]
179
+ operation = 'EXECUTE'
180
+ end
181
+ end
182
+
183
+ attrs = { 'db.operation' => validated_operation(operation), 'db.postgresql.prepared_statement_name' => statement_name }
184
+ attrs['db.statement'] = sql if config[:epsagon][:metadata_only] == false
185
+ attrs['db.sql.table'] = table_name(sql)
186
+ attrs.reject! { |_, v| v.nil? }
187
+
188
+ [database_name, client_attributes.merge(attrs)]
189
+ end
190
+
191
+ def table_name(sql)
192
+ return '' if sql.nil?
193
+
194
+ parsed_query = PgQuery.parse(sql)
195
+ if parsed_query.tables.length == 0
196
+ ''
197
+ else
198
+ parsed_query.tables[0]
199
+ end
200
+ rescue PgQuery::ParseError
201
+ ''
202
+ end
203
+
204
+ def validated_operation(operation)
205
+ operation if PostgresExtension::SQL_COMMANDS.include?(operation)
206
+ end
207
+
208
+ def extract_operation(sql)
209
+ # From: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/9244a08a8d014afe26b82b91cf86e407c2599d73/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts#L35
210
+ sql.to_s.split[0].to_s.upcase
211
+ end
212
+
213
+ def generated_postgres_regex
214
+ @generated_postgres_regex ||= Regexp.union(PostgresExtension::POSTGRES_COMPONENTS.map { |component| PostgresExtension::COMPONENTS_REGEX_MAP[component] })
215
+ end
216
+
217
+ def database_name
218
+ conninfo_hash[:dbname]&.to_s
219
+ end
220
+
221
+ def client_attributes
222
+ attributes = {
223
+ 'db.system' => 'postgresql',
224
+ 'db.user' => conninfo_hash[:user]&.to_s,
225
+ 'db.name' => database_name,
226
+ 'net.peer.name' => conninfo_hash[:host]&.to_s
227
+ }
228
+ # attributes['peer.service'] = config[:peer_service] # if config[:peer_service]
229
+
230
+ attributes.merge(transport_attrs).reject { |_, v| v.nil? }
231
+ end
232
+
233
+ def transport_attrs
234
+ if conninfo_hash[:host]&.start_with?('/')
235
+ { 'net.transport' => 'Unix' }
236
+ else
237
+ {
238
+ 'net.transport' => 'IP.TCP',
239
+ 'net.peer.ip' => conninfo_hash[:hostaddr]&.to_s,
240
+ 'net.peer.port' => conninfo_hash[:port]&.to_s
241
+ }
242
+ end
243
+ end
244
+ end
245
+
246
+ # Copyright The OpenTelemetry Authors
247
+ #
248
+ # SPDX-License-Identifier: Apache-2.0
249
+ # A simple LRU cache for the postgres instrumentation.
250
+ class LruCache
251
+ # Rather than take a dependency on another gem, we implement a very, very basic
252
+ # LRU cache here. We can take advantage of the fact that Ruby hashes are ordered
253
+ # to always keep the recently-accessed keys at the top.
254
+ def initialize(size)
255
+ raise ArgumentError, 'Invalid size' if size < 1
256
+
257
+ @limit = size
258
+ @store = {}
259
+ end
260
+
261
+ def [](key)
262
+ # We need to check for the key explicitly, because `nil` is a valid hash value.
263
+ return unless @store.key?(key)
264
+
265
+ # Since the cache contains the item, we delete and re-insert into the hash.
266
+ # This guarantees that hash keys are ordered by access recency.
267
+ value = @store.delete(key)
268
+ @store[key] = value
269
+
270
+ value
271
+ end
272
+
273
+ def []=(key, value)
274
+ # We remove the value if it's already present, so that the hash keys remain ordered
275
+ # by access recency.
276
+ @store.delete(key)
277
+ @store[key] = value
278
+ @store.shift if @store.length > @limit
279
+ end
280
+ end
281
+
282
+ #
283
+ # EpsagonPostgresInstrumentation
284
+ # Installs the Instrumentation on the PG::Connection class
285
+ #
286
+ class EpsagonPostgresInstrumentation < OpenTelemetry::Instrumentation::Base
287
+ install do |_config|
288
+ ::PG::Connection.prepend(PostgresExtension)
289
+ end
290
+
291
+ present do
292
+ defined?(::PG)
293
+ end
294
+ end
@@ -1,58 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'opentelemetry'
4
- require 'rails'
5
- require 'action_controller/railtie'
6
4
 
7
5
  require_relative '../util'
8
6
  require_relative '../epsagon_constants'
9
7
 
10
8
 
11
- module RackExtension
12
- module_function
13
-
14
- CURRENT_SPAN_KEY = OpenTelemetry::Context.create_key('current-span')
15
-
16
- private_constant :CURRENT_SPAN_KEY
17
-
18
- # Returns the current span from the current or provided context
19
- #
20
- # @param [optional Context] context The context to lookup the current
21
- # {Span} from. Defaults to Context.current
22
- def current_span(context = nil)
23
- context ||= OpenTelemetry::Context.current
24
- context.value(CURRENT_SPAN_KEY) || OpenTelemetry::Trace::Span::INVALID
25
- end
26
-
27
- # Returns a context containing the span, derived from the optional parent
28
- # context, or the current context if one was not provided.
29
- #
30
- # @param [optional Context] context The context to use as the parent for
31
- # the returned context
32
- def context_with_span(span, parent_context: OpenTelemetry::Context.current)
33
- parent_context.set_value(CURRENT_SPAN_KEY, span)
34
- end
35
-
36
- # Activates/deactivates the Span within the current Context, which makes the "current span"
37
- # available implicitly.
38
- #
39
- # On exit, the Span that was active before calling this method will be reactivated.
40
- #
41
- # @param [Span] span the span to activate
42
- # @yield [span, context] yields span and a context containing the span to the block.
43
- def with_span(span)
44
- OpenTelemetry::Context.with_value(CURRENT_SPAN_KEY, span) { |c, s| yield s, c }
45
- end
46
- end
47
-
48
- module MetalPatch
49
- def dispatch(name, request, response)
50
- rack_span = RackExtension.current_span
51
- # rack_span.name = "#{self.class.name}##{name}" if rack_span.context.valid? && !request.env['action_dispatch.exception']
52
- super(name, request, response)
53
- end
54
- end
55
-
56
9
  class EpsagonRailsInstrumentation < OpenTelemetry::Instrumentation::Base
57
10
  install do |_config|
58
11
  require_relative 'epsagon_rails_middleware'
@@ -0,0 +1,21 @@
1
+ class EpsagonResqueInstrumentation < OpenTelemetry::Instrumentation::Base
2
+ install do |_config|
3
+ require_dependencies
4
+ patch
5
+ end
6
+
7
+ present do
8
+ defined?(::Resque)
9
+ end
10
+
11
+ private
12
+
13
+ def patch
14
+ ::Resque.prepend(EpsagonResqueModule)
15
+ ::Resque::Job.prepend(EpsagonResqueJob)
16
+ end
17
+
18
+ def require_dependencies
19
+ require_relative 'epsagon_resque_job'
20
+ end
21
+ end
data/lib/util.rb CHANGED
@@ -4,6 +4,10 @@ require 'cgi'
4
4
 
5
5
  # Utilities for epsagon opentelemetry solution
6
6
  module Util
7
+ def self.validate_value(h, k, message, &block)
8
+ raise ArgumentError.new( "#{k} #{message}. Got #{h[k].class}: #{h[k]}" ) unless yield(h[k])
9
+ end
10
+
7
11
  def self.epsagon_query_attributes(query_string)
8
12
  if query_string&.include? '='
9
13
  { 'http.request.query_params' => CGI.parse(query_string).to_json }
@@ -12,6 +16,30 @@ module Util
12
16
  end
13
17
  end
14
18
 
19
+
20
+ def self.remove_key_recursive(h, key)
21
+ dot_idx = key.index('.')
22
+ if not dot_idx.nil?
23
+ next_hash = h[key[0..dot_idx - 1]]
24
+ self.remove_key_recursive(next_hash, key[dot_idx + 1..-1]) if next_hash
25
+ else
26
+ h.delete(key)
27
+ end
28
+ end
29
+
30
+ def self.prepare_attr(key, value, max_size, excluded_keys)
31
+ return nil if excluded_keys.include? key
32
+ return self.trim_attr(value, max_size) unless value.instance_of? Hash
33
+ value = value.dup
34
+ excluded_keys.each do |ekey|
35
+ if ekey.start_with? (key + '.')
36
+ rest_of_key = ekey[key.size + 1..-1]
37
+ self.remove_key_recursive(value, rest_of_key)
38
+ end
39
+ end
40
+ return self.trim_attr(JSON.dump(value), max_size)
41
+ end
42
+
15
43
  def self.trim_attr(value, max_size)
16
44
  if value.instance_of? Array then
17
45
  current_size = 2
@@ -25,7 +53,7 @@ module Util
25
53
  end
26
54
  return value
27
55
  elsif value.instance_of? String then
28
- value[0, max_size]
56
+ (value.frozen? ? value.dup : value).force_encoding('utf-8')[0, max_size]
29
57
  else
30
58
  value
31
59
  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.26
4
+ version: 0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Epsagon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-01 00:00:00.000000000 Z
11
+ date: 2021-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-api
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.11.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg_query
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
83
97
  description: 'Epsagon provides tracing to Ruby applications for the collection of
84
98
  distributed tracing and performance metrics to simplify complex architectures, eliminate
85
99
  manual work, visualize and correlate data to identify and fix problems fast.
@@ -90,6 +104,7 @@ executables: []
90
104
  extensions: []
91
105
  extra_rdoc_files: []
92
106
  files:
107
+ - lib/arn_parser.rb
93
108
  - lib/epsagon.rb
94
109
  - lib/epsagon_constants.rb
95
110
  - lib/exporter_extension.rb
@@ -97,9 +112,12 @@ files:
97
112
  - lib/instrumentation/aws_sdk_plugin.rb
98
113
  - lib/instrumentation/epsagon_faraday_middleware.rb
99
114
  - lib/instrumentation/epsagon_rails_middleware.rb
115
+ - lib/instrumentation/epsagon_resque_job.rb
100
116
  - lib/instrumentation/faraday.rb
101
117
  - lib/instrumentation/net_http.rb
118
+ - lib/instrumentation/postgres.rb
102
119
  - lib/instrumentation/rails.rb
120
+ - lib/instrumentation/resque.rb
103
121
  - lib/instrumentation/sinatra.rb
104
122
  - lib/instrumentation/version.rb
105
123
  - lib/util.rb