epsagon 0.0.24 → 0.0.29

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: 4f90417afae2709a9a4dbc88f2edab85cea49ff77577048808401f981cba0a96
4
- data.tar.gz: '0811d976ea61bce684d2da4322fb2493b37b58af0c269b1ff785cdede6b5fad2'
3
+ metadata.gz: 25cf09d9ea5babb3e7caa3380d6b8a78365909391ec3eb84ec150e07fbd9841a
4
+ data.tar.gz: bb3f2efc49e2b0930ffb677bfb5b8440fa16fe2a41a7adb96b33a35e89efdb67
5
5
  SHA512:
6
- metadata.gz: 560b2ea93192ed4502f261433728983544f3696378c846df19734602187335f6a155b52bbbaf7c424aca161727b09b37477b7f5a25b2d0711045a9a6c61b40dd
7
- data.tar.gz: 5f3a848235508e9adc51555d6ebf2c107dcedde0aba4a0da37d961e37fa143064968cceca46170e9bdd92fc3bcbd8f980f4b7cbf9d7503eb2b77659e98d976fe
6
+ metadata.gz: b179e2cf5b65329b6a7a40dec43a379fb294ddee0e58f389cd4ffc105f503265ad655cbf50067a9995b04358ddb9d9d0f8441d47e2639befcc4744c8af3c290c
7
+ data.tar.gz: 5b8443325b4afadffed3f836ba9a7385cbdfbc2cd03eebfee007bb0543ddb5f2b3c279d22a614fa80ecabb7f4a18f6a1292298567d444a84c9a364ef361bb626
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,73 +12,121 @@ 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'
19
+ require_relative 'exporter_extension'
20
+ require_relative 'arn_parser'
17
21
 
18
22
  Bundler.require
19
23
 
24
+
20
25
  # Epsagon tracing main entry point
21
26
  module Epsagon
22
-
23
27
  DEFAULT_BACKEND = 'opentelemetry.tc.epsagon.com:443/traces'
28
+ DEFAULT_IGNORE_DOMAINS = ['newrelic.com'].freeze
29
+ MUTABLE_CONF_KEYS = Set.new([:metadata_only, :max_attribute_size, :ignore_domains])
30
+
24
31
 
25
- @@epsagon_config = {
26
- metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
27
- debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
28
- token: ENV['EPSAGON_TOKEN'],
29
- app_name: ENV['EPSAGON_APP_NAME'],
30
- max_attribute_size: ENV['EPSAGON_MAX_ATTRIBUTE_SIZE'] || 5000,
31
- backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND
32
- }
32
+ @@epsagon_config = nil
33
33
 
34
34
  module_function
35
35
 
36
36
  def init(**args)
37
- @@epsagon_config.merge!(args)
37
+ get_config.merge!(args)
38
+ validate(get_config)
38
39
  OpenTelemetry::SDK.configure
40
+ @@initialized = true
41
+ end
42
+
43
+ def validate(config)
44
+ Util.validate_value(config, :metadata_only, 'Must be a boolean') {|v| !!v == v}
45
+ Util.validate_value(config, :debug, 'Must be a boolean') {|v| !!v == v}
46
+ Util.validate_value(config, :token, 'Must be a valid Epsagon token') {|v| v.is_a? String and v.size > 10}
47
+ Util.validate_value(config, :app_name, 'Must be a String') {|v| v.is_a? String}
48
+ Util.validate_value(config, :max_attribute_size, 'Must be an Integer') {|v| v.is_a? Integer}
49
+ Util.validate_value(config, :ignore_domains, 'Must be iterable') {|v| v.respond_to?(:each)}
50
+ Util.validate_value(config, :ignore_domains, 'Must be iterable') {|v| v.respond_to?(:each)}
51
+ end
52
+
53
+ def set_config(**args)
54
+ unless args.keys.all? {|a| MUTABLE_CONF_KEYS.include?(a)}
55
+ raise ArgumentError("only #{MUTABLE_CONF_KEYS.to_a} are mutable after `Epsagon.init`")
56
+ end
57
+ Epsagon.init unless @@initialized
58
+ new_conf = get_config.merge(args)
59
+ validate(new_conf)
60
+ @@epsagon_config = new_conf
39
61
  end
40
62
 
41
63
  def get_config
42
- @@epsagon_config
64
+ @@epsagon_config ||= {
65
+ metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
66
+ debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
67
+ token: ENV['EPSAGON_TOKEN'] || '',
68
+ app_name: ENV['EPSAGON_APP_NAME'] || '',
69
+ max_attribute_size: ENV['EPSAGON_MAX_ATTRIBUTE_SIZE'] || 5000,
70
+ backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND,
71
+ ignore_domains: ENV['EPSAGON_IGNORE_DOMAINS'] || DEFAULT_IGNORE_DOMAINS
72
+ }
73
+ end
74
+
75
+ def set_ecs_metadata
76
+ metadata_uri = ENV['ECS_CONTAINER_METADATA_URI']
77
+ return {} if metadata_uri.nil?
78
+
79
+ response = Net::HTTP.get(URI(metadata_uri))
80
+ ecs_metadata = JSON.parse(response)
81
+ arn = Arn.parse(ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'])
82
+
83
+ {
84
+ 'aws.account_id' => arn.account,
85
+ 'aws.region' => arn.region,
86
+ 'aws.ecs.cluster' => ecs_metadata['Labels']['com.amazonaws.ecs.cluster'],
87
+ 'aws.ecs.task_arn' => ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'],
88
+ 'aws.ecs.container_name' => ecs_metadata['Labels']['com.amazonaws.ecs.container-name'],
89
+ 'aws.ecs.task.family' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-family'],
90
+ 'aws.ecs.task.revision' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-version']
91
+ }
43
92
  end
44
93
 
45
94
  # config opentelemetry with epsaon extensions:
46
95
 
47
96
  def epsagon_confs(configurator)
97
+ otel_resource = {
98
+ 'application' => get_config[:app_name],
99
+ 'epsagon.version' => EpsagonConstants::VERSION,
100
+ 'epsagon.metadata_only' => get_config[:metadata_only]
101
+ }.merge(set_ecs_metadata)
102
+
48
103
  configurator.resource = OpenTelemetry::SDK::Resources::Resource.telemetry_sdk.merge(
49
- OpenTelemetry::SDK::Resources::Resource.create({
50
- 'application' => @@epsagon_config[:app_name],
51
- 'epsagon.version' => EpsagonConstants::VERSION
52
- })
104
+ OpenTelemetry::SDK::Resources::Resource.create(otel_resource)
53
105
  )
54
- configurator.use 'EpsagonSinatraInstrumentation', { epsagon: @@epsagon_config }
55
- configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: @@epsagon_config }
56
- configurator.use 'EpsagonFaradayInstrumentation', { epsagon: @@epsagon_config }
57
- configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: @@epsagon_config }
58
- configurator.use 'EpsagonRailsInstrumentation', { epsagon: @@epsagon_config }
59
- configurator.use 'OpenTelemetry::Instrumentation::Sidekiq', { epsagon: @@epsagon_config }
60
-
61
- if @@epsagon_config[:debug]
62
- configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
63
- OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
64
- 'x-epsagon-token' => @@epsagon_config[:token]
65
- },
66
- endpoint: @@epsagon_config[:backend],
67
- insecure: @@epsagon_config[:insecure] || false)
68
- )
69
106
 
107
+ configurator.use 'EpsagonSinatraInstrumentation', { epsagon: get_config }
108
+ configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: get_config }
109
+ configurator.use 'EpsagonFaradayInstrumentation', { epsagon: get_config }
110
+ configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: get_config }
111
+ configurator.use 'EpsagonRailsInstrumentation', { epsagon: get_config }
112
+ configurator.use 'OpenTelemetry::Instrumentation::Sidekiq', { epsagon: get_config }
113
+ configurator.use 'EpsagonPostgresInstrumentation', { epsagon: get_config }
114
+ configurator.use 'EpsagonResqueInstrumentation', { epsagon: get_config }
115
+
116
+
117
+ if get_config[:debug]
70
118
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
71
119
  OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new
72
120
  )
73
- else
74
- configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
75
- exporter: OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
76
- 'x-epsagon-token' => @@epsagon_config[:token]
77
- },
78
- endpoint: @@epsagon_config[:backend],
79
- insecure: @@epsagon_config[:insecure] || false)
80
- )
81
121
  end
122
+
123
+ configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
124
+ exporter: OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
125
+ 'x-epsagon-token' => get_config[:token]
126
+ },
127
+ endpoint: get_config[:backend],
128
+ insecure: get_config[:insecure] || false)
129
+ )
82
130
  end
83
131
  end
84
132
 
@@ -97,16 +145,14 @@ module SpanExtension
97
145
  def initialize(*args)
98
146
  super(*args)
99
147
  if @attributes
100
- @attributes = Hash[@attributes.map { |k,v|
148
+ @attributes = Hash[@attributes.select {|k,v| not BLANKS.include? v}.map { |k,v|
101
149
  [k, Util.trim_attr(v, Epsagon.get_config[:max_attribute_size])]
102
150
  }]
103
151
  end
104
-
105
152
  end
106
153
  end
107
154
 
108
155
  module SidekiqClientMiddlewareExtension
109
-
110
156
  def call(_worker_class, job, _queue, _redis_pool)
111
157
  config = OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.config[:epsagon] || {}
112
158
  attributes = {
@@ -137,6 +183,7 @@ end
137
183
 
138
184
  module SidekiqServerMiddlewareExtension
139
185
  def call(_worker, msg, _queue)
186
+ inner_exception = nil
140
187
  config = OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.config[:epsagon] || {}
141
188
  parent_context = OpenTelemetry.propagation.text.extract(msg)
142
189
  attributes = {
@@ -148,6 +195,11 @@ module SidekiqServerMiddlewareExtension
148
195
  'messaging.destination_kind' => 'queue',
149
196
  'messaging.sidekiq.redis_url' => Sidekiq.options['url'] || Util.redis_default_url
150
197
  }
198
+ runner_attributes = {
199
+ 'type' => 'sidekiq_worker',
200
+ 'messaging.sidekiq.redis_url' => Sidekiq.options['url'] || Util.redis_default_url,
201
+
202
+ }
151
203
  unless config[:metadata_only]
152
204
  attributes.merge!({
153
205
  'messaging.sidekiq.args' => JSON.dump(msg['args'])
@@ -158,11 +210,19 @@ module SidekiqServerMiddlewareExtension
158
210
  attributes: attributes,
159
211
  with_parent: parent_context,
160
212
  kind: :consumer
161
- ) do |span|
162
- span.add_event('created_at', timestamp: msg['created_at'])
163
- span.add_event('enqueued_at', timestamp: msg['enqueued_at'])
164
- yield
213
+ ) do |trigger_span|
214
+ trigger_span.add_event('created_at', timestamp: msg['created_at'])
215
+ trigger_span.add_event('enqueued_at', timestamp: msg['enqueued_at'])
216
+ tracer.in_span(msg['wrapped']&.to_s || msg['class'],
217
+ attributes: runner_attributes,
218
+ kind: :consumer
219
+ ) do |runner_span|
220
+ yield
221
+ end
222
+ rescue Exception => e
223
+ inner_exception = e
165
224
  end
225
+ raise inner_exception if inner_exception
166
226
  end
167
227
  end
168
228
 
@@ -1,3 +1,3 @@
1
1
  module EpsagonConstants
2
- VERSION = '0.0.24'
2
+ VERSION = '0.0.29'
3
3
  end
@@ -0,0 +1,78 @@
1
+
2
+
3
+ module OpenTelemetry
4
+ module Exporter
5
+ module OTLP
6
+ # An OpenTelemetry trace exporter that sends spans over HTTP as Protobuf encoded OTLP ExportTraceServiceRequests.
7
+ class Exporter # rubocop:disable Metrics/ClassLength
8
+ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
9
+ retry_count = 0
10
+ timeout ||= @timeout
11
+ start_time = Time.now
12
+ untraced do # rubocop:disable Metrics/BlockLength
13
+ request = Net::HTTP::Post.new(@path)
14
+ request.body = if @compression == 'gzip'
15
+ request.add_field('Content-Encoding', 'gzip')
16
+ Zlib.gzip(bytes)
17
+ else
18
+ bytes
19
+ end
20
+ request.add_field('Content-Type', 'application/x-protobuf')
21
+ @headers&.each { |key, value| request.add_field(key, value) }
22
+
23
+ remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time)
24
+ return TIMEOUT if remaining_timeout.zero?
25
+
26
+ @http.open_timeout = remaining_timeout
27
+ @http.read_timeout = remaining_timeout
28
+ @http.write_timeout = remaining_timeout if WRITE_TIMEOUT_SUPPORTED
29
+ @http.start unless @http.started?
30
+ response = measure_request_duration { @http.request(request) }
31
+
32
+ case response
33
+ when Net::HTTPOK
34
+ response.body # Read and discard body
35
+ SUCCESS
36
+ when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests
37
+ response.body # Read and discard body
38
+ redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1, reason: response.code)
39
+ FAILURE
40
+ when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway
41
+ response.body # Read and discard body
42
+ redo if backoff?(retry_count: retry_count += 1, reason: response.code)
43
+ FAILURE
44
+ when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError
45
+ # TODO: decode the body as a google.rpc.Status Protobuf-encoded message when https://github.com/open-telemetry/opentelemetry-collector/issues/1357 is fixed.
46
+ response.body # Read and discard body
47
+ FAILURE
48
+ when Net::HTTPRedirection
49
+ @http.finish
50
+ handle_redirect(response['location'])
51
+ redo if backoff?(retry_after: 0, retry_count: retry_count += 1, reason: response.code)
52
+ else
53
+ @http.finish
54
+ FAILURE
55
+ end
56
+ rescue Net::OpenTimeout, Net::ReadTimeout
57
+ puts "Epsagon: timeout while sending trace" if Epsagon.get_config[:debug]
58
+ retry if backoff?(retry_count: retry_count += 1, reason: 'timeout')
59
+ return FAILURE
60
+ ensure
61
+ if Epsagon.get_config[:debug] && response && response.code.to_i >= 400
62
+ puts "Epsagon: Error while sending trace:"
63
+ puts "#{response.code} #{response.class.name} #{response.message}"
64
+ puts "Headers: #{response.to_hash.inspect}"
65
+ puts response.body
66
+ end
67
+ end
68
+ ensure
69
+ # Reset timeouts to defaults for the next call.
70
+ @http.open_timeout = @timeout
71
+ @http.read_timeout = @timeout
72
+ @http.write_timeout = @timeout if WRITE_TIMEOUT_SUPPORTED
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
@@ -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
@@ -4,6 +4,7 @@ require 'aws-sdk-core'
4
4
  require 'opentelemetry/common'
5
5
  require 'opentelemetry/sdk'
6
6
 
7
+
7
8
  def untraced(&block)
8
9
  OpenTelemetry::Trace.with_span(OpenTelemetry::Trace::Span.new, &block)
9
10
  end
@@ -17,14 +18,120 @@ end
17
18
 
18
19
  # Generates Spans for all uses of AWS SDK
19
20
  class EpsagonAwsHandler < Seahorse::Client::Handler
21
+ SPAN_KIND = {
22
+ 'ReceiveMessage' => :consumer,
23
+ 'SendMessage' => :producer,
24
+ 'SendMessageBatch' => :producer,
25
+ 'Publish' => :producer,
26
+ }
27
+
20
28
  def call(context)
21
- tracer.in_span('', kind: :client) do |span|
29
+ span_name = ''
30
+ span_kind = :client
31
+ attributes = {
32
+ 'aws.service' => context.client.class.to_s.split('::')[1].downcase,
33
+ 'aws.operation' => context.operation.name
34
+ }
35
+ attributes['aws.region'] = context.client.config.region unless attributes['aws.service'] == 's3'
36
+
37
+ span_kind = SPAN_KIND[attributes['aws.operation']] || span_kind
38
+ if attributes['aws.service'] == 's3'
39
+ attributes['aws.s3.bucket'] = context.params[:bucket]
40
+ span_name = attributes['aws.s3.bucket'] if attributes['aws.s3.bucket']
41
+ attributes['aws.s3.key'] = context.params[:key]
42
+ attributes['aws.s3.copy_source'] = context.params[:copy_source]
43
+ elsif attributes['aws.service'] == 'sqs'
44
+ queue_url = context.params[:queue_url]
45
+ queue_name = queue_url ? queue_url[queue_url.rindex('/')+1..-1] : context.params[:queue_name]
46
+ attributes['aws.sqs.max_number_of_messages'] = context.params[:max_number_of_messages]
47
+ attributes['aws.sqs.wait_time_seconds'] = context.params[:wait_time_seconds]
48
+ attributes['aws.sqs.visibility_timeout'] = context.params[:visibility_timeout]
49
+ attributes['aws.sqs.message_id'] = context.params[:message_id]
50
+ if queue_name
51
+ attributes['aws.sqs.queue_name'] = queue_name
52
+ span_name = attributes['aws.sqs.queue_name'] if attributes['aws.sqs.queue_name']
53
+ end
54
+ unless config[:epsagon][:metadata_only]
55
+ if attributes['aws.operation'] == 'SendMessageBatch'
56
+ messages_attributes = context.params[:entries].map do |m|
57
+ record = {
58
+ 'message_attributes' => m[:message_attributes].map {|k,v| [k, v.to_h]},
59
+ 'message_body' => m[:message_body],
60
+ }
61
+ end
62
+ attributes['aws.sqs.record'] = JSON.dump(messages_attributes) if messages_attributes
63
+ end
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]
66
+ end
67
+ elsif attributes['aws.service'] == 'sns'
68
+ topic_arn = context.params[:topic_arn]
69
+ topic_name = topic_arn ? topic_arn[topic_arn.rindex(':')+1..-1] : context.params[:name]
70
+ span_name = attributes['aws.sns.topic_name'] = topic_name if topic_name
71
+ unless config[:epsagon][:metadata_only]
72
+ attributes['aws.sns.subject'] = context.params[:subject]
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]
75
+ end
76
+ end
77
+ tracer.in_span(span_name, kind: span_kind, attributes: attributes) do |span|
22
78
  untraced do
23
- @handler.call(context).tap do
24
- span.set_attribute('aws.service', context.client.class.to_s.split('::')[1].downcase)
25
- span.set_attribute('aws.operation', context.operation.name)
26
- span.set_attribute('aws.region', context.client.config.region)
79
+ @handler.call(context).tap do |result|
80
+ if attributes['aws.service'] == 's3'
81
+ modified = context.http_response.headers[:'last-modified']
82
+ reformatted_modified = modified ?
83
+ Time.strptime(modified, '%a, %d %b %Y %H:%M:%S %Z')
84
+ .strftime('%Y-%m-%dT%H:%M:%SZ') :
85
+ nil
86
+ if context.operation.name == 'GetObject'
87
+ span.set_attribute('aws.s3.content_length', context.http_response.headers[:'content-length']&.to_i)
88
+ end
89
+ span.set_attribute('aws.s3.etag', context.http_response.headers[:etag]&.tr('"',''))
90
+ span.set_attribute('aws.s3.last_modified', reformatted_modified)
91
+ elsif attributes['aws.service'] == 'sqs'
92
+ if context.operation.name == 'SendMessage'
93
+ span.set_attribute('aws.sqs.record.message_id', result.message_id)
94
+ end
95
+ if context.operation.name == 'SendMessageBatch'
96
+ messages_attributes = result.successful.map do |m|
97
+ record = {'message_id' => m.message_id}
98
+ unless config[:epsagon][:metadata_only]
99
+ context.params[:entries].each do |e|
100
+ record.merge!({
101
+ 'message_attributes' => e[:message_attributes].map {|k,v| [k, v.to_h]},
102
+ 'message_body' => e[:message_body],
103
+ }) if e[:id] == m.id
104
+ end
105
+ end
106
+ record
107
+ end
108
+ span.set_attribute('aws.sqs.record', JSON.dump(messages_attributes)) if messages_attributes
109
+ end
110
+ if context.operation.name == 'ReceiveMessage'
111
+ messages_attributes = result.messages.map do |m|
112
+ record = {
113
+ 'message_id' => m.message_id,
114
+ 'attributes' => {
115
+ 'sender_id' => m.attributes['SenderId'],
116
+ 'sent_timestamp' => m.attributes['SentTimestamp'],
117
+ 'aws_trace_header' => m.attributes['AWSTraceHeader'],
118
+ }
119
+ }
120
+ unless config[:epsagon][:metadata_only]
121
+ record['message_attributes'] = m.message_attributes.map {|k,v| [k, v.to_h]}
122
+ record['message_body'] = m.body
123
+ end
124
+ record
125
+ end
126
+ span.set_attribute('aws.sqs.record', JSON.dump(messages_attributes)) if messages_attributes
127
+ end
128
+ elsif attributes['aws.service'] == 'sns'
129
+ span.set_attribute('aws.sns.message_id', result.message_id) if context.operation.name == 'Publish'
130
+ end
27
131
  span.set_attribute('http.status_code', context.http_response.status_code)
132
+ span.status = OpenTelemetry::Trace::Status.http_to_status(
133
+ context.http_response.status_code
134
+ )
28
135
  end
29
136
  end
30
137
  end
@@ -33,4 +140,8 @@ class EpsagonAwsHandler < Seahorse::Client::Handler
33
140
  def tracer
34
141
  EpsagonAwsSdkInstrumentation.instance.tracer()
35
142
  end
143
+
144
+ def config
145
+ EpsagonAwsSdkInstrumentation.instance.config
146
+ end
36
147
  end
@@ -37,11 +37,11 @@ class EpsagonFaradayMiddleware < ::Faraday::Middleware
37
37
  unless config[:epsagon][:metadata_only]
38
38
  attributes.merge!(Util.epsagon_query_attributes(env.url.query))
39
39
  attributes.merge!({
40
- 'http.request.path_params' => path_params,
41
- 'http.request.headers' => env.request_headers.to_json,
42
- 'http.request.body' => env.body,
43
- 'http.request.headers.User-Agent' => env.request_headers['User-Agent']
44
- })
40
+ 'http.request.path_params' => path_params,
41
+ 'http.request.headers' => env.request_headers.to_json,
42
+ 'http.request.body' => env.body,
43
+ 'http.request.headers.User-Agent' => env.request_headers['User-Agent']
44
+ })
45
45
  end
46
46
 
47
47
  tracer.in_span(
@@ -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'
@@ -130,7 +131,7 @@ class EpsagonRackMiddleware
130
131
  def request_span_attributes(env:)
131
132
  request = Rack::Request.new(env)
132
133
  path, path_params = request.path.split(';')
133
- request_headers = JSON.generate(Hash[*env.select { |k, _v| k.start_with? 'HTTP_' }
134
+ request_headers = JSON.generate(Hash[*env.select { |k, _v| k.to_s.start_with? 'HTTP_' }
134
135
  .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
135
136
  .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
136
137
  .sort
@@ -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' => JSON.dump(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' => JSON.dump(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
@@ -17,6 +17,7 @@ module EpsagonNetHTTPExtension
17
17
  def request(req, body = nil, &block)
18
18
  # Do not trace recursive call for starting the connection
19
19
  return super(req, body, &block) unless started?
20
+ return super(req, body, &block) if config[:epsagon][:ignore_domains].any? {|d| @address.include? d}
20
21
 
21
22
  attributes = Hash[OpenTelemetry::Common::HTTP::ClientContext.attributes]
22
23
  path_with_params, query = req.path.split('?')
@@ -55,6 +56,7 @@ module EpsagonNetHTTPExtension
55
56
 
56
57
  def annotate_span_with_response!(span, response)
57
58
  return unless response&.code
59
+ return unless span.respond_to?(:set_attribute)
58
60
 
59
61
  status_code = response.code.to_i
60
62
 
@@ -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 }
@@ -25,7 +29,7 @@ module Util
25
29
  end
26
30
  return value
27
31
  elsif value.instance_of? String then
28
- value[0, max_size]
32
+ (value.frozen? ? value.dup : value).force_encoding('utf-8')[0, max_size]
29
33
  else
30
34
  value
31
35
  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.24
4
+ version: 0.0.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Epsagon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-12 00:00:00.000000000 Z
11
+ date: 2021-07-01 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,15 +104,20 @@ 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
110
+ - lib/exporter_extension.rb
95
111
  - lib/instrumentation/aws_sdk.rb
96
112
  - lib/instrumentation/aws_sdk_plugin.rb
97
113
  - lib/instrumentation/epsagon_faraday_middleware.rb
98
114
  - lib/instrumentation/epsagon_rails_middleware.rb
115
+ - lib/instrumentation/epsagon_resque_job.rb
99
116
  - lib/instrumentation/faraday.rb
100
117
  - lib/instrumentation/net_http.rb
118
+ - lib/instrumentation/postgres.rb
101
119
  - lib/instrumentation/rails.rb
120
+ - lib/instrumentation/resque.rb
102
121
  - lib/instrumentation/sinatra.rb
103
122
  - lib/instrumentation/version.rb
104
123
  - lib/util.rb