epsagon 0.0.23 → 0.0.28

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: 9067624373427de33a82ba42e8b58bcf6d102ed85b8aaf24c69f511e628f2329
4
- data.tar.gz: 2532583c2fb18426b046df27fca70adeff280c9ca6178e5188b905e6e17caa06
3
+ metadata.gz: 1a17b55f2ce2ff35ca70dd51e8f8337041e202c528b6c691800d3e3b7b0d42c4
4
+ data.tar.gz: d93046c571faa2d35a681217cb12952c57c7bd3232e77a136a5954a5534541b1
5
5
  SHA512:
6
- metadata.gz: e1b93087e00032717588067013467eacc613418e20792aa86c36d2840d6fc13052971793d34bf4c8c47a83e677a174adfd2d6f45e150de058910214e19596799
7
- data.tar.gz: 2711a7e340eb7bc768970de0cb88112dd743ecbc843285c1d3daaf4e437b6cec30550dea24f8232dce008209d78a9d05ed42b9cc2f70eca8eb3315011ce48f9f
6
+ metadata.gz: 9290a8dee89f24588f93bca0ab331855cab0cd46e09fdae10c4bb0c00c7c61c35c6045bc6b41971639767fc46f30d241b49a45a89a51c29baaa734113188aa7a
7
+ data.tar.gz: 3c4c1b54d0b344bf7399f3d088cd22bebca010d2b063244b425693bcaa2ef0ced625ec3a0921d1d5ce0cf7effd81e90dc6e410d5c893f1062096c78ec66e795f
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
@@ -1,70 +1,98 @@
1
1
  # frozen_string_literal: true
2
-
2
+ require 'json'
3
3
  require 'rubygems'
4
4
  require 'net/http'
5
5
  require 'bundler/setup'
6
6
  require 'opentelemetry/sdk'
7
7
  require 'opentelemetry/exporter/otlp'
8
+ require 'opentelemetry/instrumentation/sidekiq'
8
9
 
9
10
  require_relative 'instrumentation/sinatra'
10
11
  require_relative 'instrumentation/net_http'
11
12
  require_relative 'instrumentation/faraday'
12
13
  require_relative 'instrumentation/aws_sdk'
13
14
  require_relative 'instrumentation/rails'
15
+ require_relative 'instrumentation/postgres'
14
16
  require_relative 'util'
15
17
  require_relative 'epsagon_constants'
18
+ require_relative 'exporter_extension'
19
+ require_relative 'arn_parser'
16
20
 
17
21
  Bundler.require
18
22
 
19
23
  # Epsagon tracing main entry point
20
24
  module Epsagon
21
-
22
25
  DEFAULT_BACKEND = 'opentelemetry.tc.epsagon.com:443/traces'
26
+ DEFAULT_IGNORE_DOMAINS = ['newrelic.com'].freeze
23
27
 
24
- @@epsagon_config = {
25
- metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
26
- debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
27
- token: ENV['EPSAGON_TOKEN'],
28
- app_name: ENV['EPSAGON_APP_NAME'],
29
- max_attribute_size: ENV['EPSAGON_MAX_ATTRIBUTE_SIZE'] || 5000,
30
- backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND
31
- }
28
+ @@epsagon_config = nil
32
29
 
33
30
  module_function
34
31
 
35
32
  def init(**args)
36
- @@epsagon_config.merge!(args)
33
+ get_config.merge!(args)
37
34
  OpenTelemetry::SDK.configure
38
35
  end
39
36
 
40
37
  def get_config
41
- @@epsagon_config
38
+ @@epsagon_config ||= {
39
+ metadata_only: ENV['EPSAGON_METADATA']&.to_s&.downcase != 'false',
40
+ debug: ENV['EPSAGON_DEBUG']&.to_s&.downcase == 'true',
41
+ token: ENV['EPSAGON_TOKEN'] || '',
42
+ app_name: ENV['EPSAGON_APP_NAME'] || '',
43
+ max_attribute_size: ENV['EPSAGON_MAX_ATTRIBUTE_SIZE'] || 5000,
44
+ backend: ENV['EPSAGON_BACKEND'] || DEFAULT_BACKEND,
45
+ ignore_domains: ENV['EPSAGON_IGNORE_DOMAINS'] || DEFAULT_IGNORE_DOMAINS
46
+ }
47
+ end
48
+
49
+ def set_ecs_metadata
50
+ metadata_uri = ENV['ECS_CONTAINER_METADATA_URI']
51
+ return {} if metadata_uri.nil?
52
+
53
+ response = Net::HTTP.get(URI(metadata_uri))
54
+ ecs_metadata = JSON.parse(response)
55
+ arn = Arn.parse(ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'])
56
+
57
+ {
58
+ 'aws.account_id' => arn.account,
59
+ 'aws.region' => arn.region,
60
+ 'aws.ecs.cluster' => ecs_metadata['Labels']['com.amazonaws.ecs.cluster'],
61
+ 'aws.ecs.task_arn' => ecs_metadata['Labels']['com.amazonaws.ecs.task-arn'],
62
+ 'aws.ecs.container_name' => ecs_metadata['Labels']['com.amazonaws.ecs.container-name'],
63
+ 'aws.ecs.task.family' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-family'],
64
+ 'aws.ecs.task.revision' => ecs_metadata['Labels']['com.amazonaws.ecs.task-definition-version']
65
+ }
42
66
  end
43
67
 
44
68
  # config opentelemetry with epsaon extensions:
45
69
 
46
70
  def epsagon_confs(configurator)
71
+ otel_resource = {
72
+ 'application' => get_config[:app_name],
73
+ 'epsagon.version' => EpsagonConstants::VERSION,
74
+ 'epsagon.metadata_only' => get_config[:metadata_only]
75
+ }.merge(set_ecs_metadata)
76
+
47
77
  configurator.resource = OpenTelemetry::SDK::Resources::Resource.telemetry_sdk.merge(
48
- OpenTelemetry::SDK::Resources::Resource.create({
49
- 'application' => @@epsagon_config[:app_name],
50
- 'epsagon.version' => EpsagonConstants::VERSION
51
- })
78
+ OpenTelemetry::SDK::Resources::Resource.create(otel_resource)
52
79
  )
53
- configurator.use 'EpsagonSinatraInstrumentation', { epsagon: @@epsagon_config }
54
- configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: @@epsagon_config }
55
- configurator.use 'EpsagonFaradayInstrumentation', { epsagon: @@epsagon_config }
56
- configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: @@epsagon_config }
57
- configurator.use 'EpsagonRailsInstrumentation', { epsagon: @@epsagon_config }
58
- # configurator.use 'OpenTelemetry::Instrumentation::Sidekiq'
59
80
 
81
+ configurator.use 'EpsagonSinatraInstrumentation', { epsagon: get_config }
82
+ configurator.use 'EpsagonNetHTTPInstrumentation', { epsagon: get_config }
83
+ configurator.use 'EpsagonFaradayInstrumentation', { epsagon: get_config }
84
+ configurator.use 'EpsagonAwsSdkInstrumentation', { epsagon: get_config }
85
+ configurator.use 'EpsagonRailsInstrumentation', { epsagon: get_config }
86
+ configurator.use 'OpenTelemetry::Instrumentation::Sidekiq', { epsagon: get_config }
87
+ configurator.use 'EpsagonPostgresInstrumentation', { epsagon: get_config }
60
88
 
61
- if @@epsagon_config[:debug]
89
+ if get_config[:debug]
62
90
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
63
91
  OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
64
- 'x-epsagon-token' => @@epsagon_config[:token]
92
+ 'x-epsagon-token' => get_config[:token]
65
93
  },
66
- endpoint: @@epsagon_config[:backend],
67
- insecure: @@epsagon_config[:insecure] || false)
94
+ endpoint: get_config[:backend],
95
+ insecure: get_config[:insecure] || false)
68
96
  )
69
97
 
70
98
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
@@ -73,10 +101,10 @@ module Epsagon
73
101
  else
74
102
  configurator.add_span_processor OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
75
103
  exporter: OpenTelemetry::Exporter::OTLP::Exporter.new(headers: {
76
- 'x-epsagon-token' => @@epsagon_config[:token]
104
+ 'x-epsagon-token' => get_config[:token]
77
105
  },
78
- endpoint: @@epsagon_config[:backend],
79
- insecure: @@epsagon_config[:insecure] || false)
106
+ endpoint: get_config[:backend],
107
+ insecure: get_config[:insecure] || false)
80
108
  )
81
109
  end
82
110
  end
@@ -97,15 +125,89 @@ module SpanExtension
97
125
  def initialize(*args)
98
126
  super(*args)
99
127
  if @attributes
100
- @attributes = Hash[@attributes.map { |k,v|
128
+ @attributes = Hash[@attributes.select {|k,v| not BLANKS.include? v}.map { |k,v|
101
129
  [k, Util.trim_attr(v, Epsagon.get_config[:max_attribute_size])]
102
130
  }]
103
131
  end
104
-
132
+ end
133
+ end
134
+
135
+ module SidekiqClientMiddlewareExtension
136
+ def call(_worker_class, job, _queue, _redis_pool)
137
+ config = OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.config[:epsagon] || {}
138
+ attributes = {
139
+ 'operation' => job['at'] ? 'perform_at' : 'perform_async',
140
+ 'messaging.system' => 'sidekiq',
141
+ 'messaging.sidekiq.job_class' => job['wrapped']&.to_s || job['class'],
142
+ 'messaging.message_id' => job['jid'],
143
+ 'messaging.destination' => job['queue'],
144
+ 'messaging.destination_kind' => 'queue',
145
+ 'messaging.sidekiq.redis_url' => Sidekiq.options['url'] || Util.redis_default_url
146
+ }
147
+ unless config[:metadata_only]
148
+ attributes.merge!({
149
+ 'messaging.sidekiq.args' => JSON.dump(job['args'])
150
+ })
151
+ end
152
+ tracer.in_span(
153
+ job['queue'],
154
+ attributes: attributes,
155
+ kind: :producer
156
+ ) do |span|
157
+ OpenTelemetry.propagation.text.inject(job)
158
+ span.add_event('created_at', timestamp: job['created_at'])
159
+ Util.untraced {yield}
160
+ end
161
+ end
162
+ end
163
+
164
+ module SidekiqServerMiddlewareExtension
165
+ def call(_worker, msg, _queue)
166
+ inner_exception = nil
167
+ config = OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.config[:epsagon] || {}
168
+ parent_context = OpenTelemetry.propagation.text.extract(msg)
169
+ attributes = {
170
+ 'operation' => 'perform',
171
+ 'messaging.system' => 'sidekiq',
172
+ 'messaging.sidekiq.job_class' => msg['wrapped']&.to_s || msg['class'],
173
+ 'messaging.message_id' => msg['jid'],
174
+ 'messaging.destination' => msg['queue'],
175
+ 'messaging.destination_kind' => 'queue',
176
+ 'messaging.sidekiq.redis_url' => Sidekiq.options['url'] || Util.redis_default_url
177
+ }
178
+ runner_attributes = {
179
+ 'type' => 'sidekiq_worker',
180
+ 'messaging.sidekiq.redis_url' => Sidekiq.options['url'] || Util.redis_default_url,
181
+
182
+ }
183
+ unless config[:metadata_only]
184
+ attributes.merge!({
185
+ 'messaging.sidekiq.args' => JSON.dump(msg['args'])
186
+ })
187
+ end
188
+ tracer.in_span(
189
+ msg['queue'],
190
+ attributes: attributes,
191
+ with_parent: parent_context,
192
+ kind: :consumer
193
+ ) do |trigger_span|
194
+ trigger_span.add_event('created_at', timestamp: msg['created_at'])
195
+ trigger_span.add_event('enqueued_at', timestamp: msg['enqueued_at'])
196
+ tracer.in_span(msg['wrapped']&.to_s || msg['class'],
197
+ attributes: runner_attributes,
198
+ kind: :consumer
199
+ ) do |runner_span|
200
+ yield
201
+ end
202
+ rescue Exception => e
203
+ inner_exception = e
204
+ end
205
+ raise inner_exception if inner_exception
105
206
  end
106
207
  end
107
208
 
108
209
  # monkey patch to include epsagon confs
210
+
109
211
  module OpenTelemetry
110
212
  # monkey patch inner SDK module
111
213
  module SDK
@@ -122,4 +224,35 @@ module OpenTelemetry
122
224
  end
123
225
  end
124
226
  end
227
+ module Instrumentation
228
+ module Sidekiq
229
+ class Instrumentation
230
+ def add_server_middleware
231
+ ::Sidekiq.configure_server do |config|
232
+ config.server_middleware do |chain|
233
+ chain.add Middlewares::Server::TracerMiddleware
234
+ end
235
+ end
236
+
237
+ if defined?(::Sidekiq::Testing) # rubocop:disable Style/GuardClause
238
+ ::Sidekiq::Testing.server_middleware do |chain|
239
+ chain.add Middlewares::Server::TracerMiddleware
240
+ end
241
+ end
242
+ end
243
+ end
244
+ module Middlewares
245
+ module Client
246
+ class TracerMiddleware
247
+ prepend SidekiqClientMiddlewareExtension
248
+ end
249
+ end
250
+ module Server
251
+ class TracerMiddleware
252
+ prepend SidekiqServerMiddlewareExtension
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
125
258
  end
@@ -1,3 +1,3 @@
1
1
  module EpsagonConstants
2
- VERSION = '0.0.23'
2
+ VERSION = '0.0.28'
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
+
@@ -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(
@@ -130,7 +130,7 @@ class EpsagonRackMiddleware
130
130
  def request_span_attributes(env:)
131
131
  request = Rack::Request.new(env)
132
132
  path, path_params = request.path.split(';')
133
- request_headers = JSON.generate(Hash[*env.select { |k, _v| k.start_with? 'HTTP_' }
133
+ request_headers = JSON.generate(Hash[*env.select { |k, _v| k.to_s.start_with? 'HTTP_' }
134
134
  .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
135
135
  .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
136
136
  .sort
@@ -189,7 +189,7 @@ class EpsagonRackMiddleware
189
189
  def set_attributes_after_request(http_span, _framework_span, status, headers, response)
190
190
  unless config[:epsagon][:metadata_only]
191
191
  http_span.set_attribute('http.response.headers', JSON.generate(headers))
192
- http_span.set_attribute('http.response.body', response.join)
192
+ http_span.set_attribute('http.response.body', response.join) if response.respond_to?(:join)
193
193
  end
194
194
 
195
195
  http_span.set_attribute('http.status_code', status)
@@ -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
File without changes
data/lib/util.rb CHANGED
@@ -30,4 +30,13 @@ module Util
30
30
  value
31
31
  end
32
32
  end
33
+
34
+ def self.redis_default_url
35
+ @@redis_default_url ||= "#{Redis::Client::DEFAULTS[:scheme]}://#{Redis::Client::DEFAULTS[:host]}:#{Redis::Client::DEFAULTS[:port]}/#{Redis::Client::DEFAULTS[:db]}"
36
+ end
37
+
38
+ def self.untraced(&block)
39
+ OpenTelemetry::Trace.with_span(OpenTelemetry::Trace::Span.new, &block)
40
+ end
41
+
33
42
  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.23
4
+ version: 0.0.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Epsagon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-04 00:00:00.000000000 Z
11
+ date: 2021-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-api
@@ -80,32 +80,50 @@ 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.
86
100
 
87
- '
101
+ '
88
102
  email: info@epsagon.com
89
103
  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
99
115
  - lib/instrumentation/faraday.rb
100
116
  - lib/instrumentation/net_http.rb
117
+ - lib/instrumentation/postgres.rb
101
118
  - lib/instrumentation/rails.rb
102
119
  - lib/instrumentation/sinatra.rb
120
+ - lib/instrumentation/version.rb
103
121
  - lib/util.rb
104
122
  homepage: https://github.com/epsagon/epsagon-ruby
105
123
  licenses:
106
124
  - MIT
107
125
  metadata: {}
108
- post_install_message:
126
+ post_install_message:
109
127
  rdoc_options: []
110
128
  require_paths:
111
129
  - lib
@@ -120,8 +138,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
138
  - !ruby/object:Gem::Version
121
139
  version: '0'
122
140
  requirements: []
123
- rubygems_version: 3.1.4
124
- signing_key:
141
+ rubygems_version: 3.0.3
142
+ signing_key:
125
143
  specification_version: 4
126
144
  summary: Epsagon provides tracing to Ruby applications for the collection of distributed
127
145
  tracing and performance metrics.