epsagon 0.0.24 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/arn_parser.rb +27 -0
- data/lib/epsagon.rb +105 -45
- data/lib/epsagon_constants.rb +1 -1
- data/lib/exporter_extension.rb +78 -0
- data/lib/instrumentation/aws_sdk.rb +1 -0
- data/lib/instrumentation/aws_sdk_plugin.rb +116 -5
- data/lib/instrumentation/epsagon_faraday_middleware.rb +5 -5
- data/lib/instrumentation/epsagon_rails_middleware.rb +48 -2
- data/lib/instrumentation/epsagon_resque_job.rb +113 -0
- data/lib/instrumentation/net_http.rb +2 -0
- data/lib/instrumentation/postgres.rb +294 -0
- data/lib/instrumentation/rails.rb +0 -47
- data/lib/instrumentation/resque.rb +21 -0
- data/lib/util.rb +5 -1
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25cf09d9ea5babb3e7caa3380d6b8a78365909391ec3eb84ec150e07fbd9841a
|
4
|
+
data.tar.gz: bb3f2efc49e2b0930ffb677bfb5b8440fa16fe2a41a7adb96b33a35e89efdb67
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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 |
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
|
data/lib/epsagon_constants.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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.
|
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-
|
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
|