hypertrace-agent 0.1.0

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.
@@ -0,0 +1,77 @@
1
+ require 'yaml'
2
+ class Hypertrace::Config::Config
3
+ include Hypertrace::Logging
4
+ attr_reader :config
5
+ def initialize
6
+ @config = load_config
7
+ end
8
+
9
+ def load_config
10
+ # Order of loading:
11
+ # 1.) Defaults
12
+ # 2.) Overriden by config file
13
+ # 3.) Overriden by env vars
14
+ config = Marshal.load(Marshal.dump(DEFAULT_AGENT_CONFIG))
15
+
16
+ file_config = load_file
17
+ config = merge_config(config, file_config)
18
+
19
+ env_config = Hypertrace::Config::Environment.load_config_from_env
20
+ config = merge_config(config, env_config)
21
+
22
+ log.info "Finalized config loaded"
23
+ log.info config
24
+
25
+ proto_config = Hypertrace::Agent::Config::V1::AgentConfig.new
26
+ enabled = Google::Protobuf::BoolValue.new({value: config[:enabled]})
27
+ proto_config.enabled = enabled
28
+ proto_config.propagation_formats += config[:propagation_formats]
29
+ service_name = Google::Protobuf::StringValue.new({value: config[:service_name]})
30
+ proto_config.service_name = service_name
31
+
32
+ proto_config.reporting = Hypertrace::Agent::Config::V1::Reporting.new
33
+ endpoint = Google::Protobuf::StringValue.new({value: config[:reporting][:endpoint]})
34
+ proto_config.reporting.endpoint = endpoint
35
+ secure = Google::Protobuf::BoolValue.new({value: config[:reporting][:secure]})
36
+ proto_config.reporting.secure = secure
37
+
38
+ proto_config.reporting.trace_reporter_type = config[:reporting][:trace_reporter_type]
39
+
40
+ proto_config.data_capture = Hypertrace::Agent::Config::V1::DataCapture.new
41
+ %i[http_headers http_body rpc_metadata rpc_body].each do |field|
42
+ message_instance = Hypertrace::Agent::Config::V1::Message.new
43
+ message_instance.request = Google::Protobuf::BoolValue.new({value:config[:data_capture][field][:request]})
44
+ message_instance.response = Google::Protobuf::BoolValue.new({value:config[:data_capture][field][:response]})
45
+ proto_config.data_capture[field.to_s] = message_instance
46
+ end
47
+ proto_config.data_capture.body_max_size_bytes = Google::Protobuf::Int32Value.new({value: config[:data_capture][:body_max_size_bytes]})
48
+
49
+ proto_config.resource_attributes.merge(config[:resource_attributes])
50
+ proto_config
51
+ end
52
+
53
+ def load_file
54
+ file_path = Hypertrace::EnvVarSettings.env_value('CONFIG_FILE')
55
+ return {} if file_path.nil?
56
+
57
+ begin
58
+ yaml_content = File.read(file_path)
59
+ config_file_contents = YAML.load(yaml_content, symbolize_names: true)
60
+ rescue => e
61
+ log.warn "failed to load config file: #{file_path}"
62
+ end
63
+ config_file_contents || {}
64
+ end
65
+
66
+ def merge_config(base_config, overriding_config)
67
+ overriding_config.each_key do |key|
68
+ if base_config.key?(key) && base_config[key].instance_of?(Hash)
69
+ base_config[key] = merge_config(base_config[key], overriding_config[key])
70
+ else
71
+ base_config[key] = overriding_config[key]
72
+ end
73
+ end
74
+
75
+ base_config
76
+ end
77
+ end
@@ -0,0 +1,78 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # source: config.proto
3
+
4
+ require 'google/protobuf'
5
+
6
+ require 'google/protobuf/wrappers_pb'
7
+ Google::Protobuf::DescriptorPool.generated_pool.build do
8
+ add_file("config.proto", :syntax => :proto3) do
9
+ add_message "hypertrace.agent.config.v1.AgentConfig" do
10
+ optional :service_name, :message, 1, "google.protobuf.StringValue"
11
+ optional :reporting, :message, 2, "hypertrace.agent.config.v1.Reporting"
12
+ optional :data_capture, :message, 3, "hypertrace.agent.config.v1.DataCapture"
13
+ repeated :propagation_formats, :enum, 4, "hypertrace.agent.config.v1.PropagationFormat"
14
+ optional :enabled, :message, 5, "google.protobuf.BoolValue"
15
+ optional :javaagent, :message, 6, "hypertrace.agent.config.v1.JavaAgent"
16
+ map :resource_attributes, :string, :string, 7
17
+ end
18
+ add_message "hypertrace.agent.config.v1.Reporting" do
19
+ optional :endpoint, :message, 1, "google.protobuf.StringValue"
20
+ optional :secure, :message, 2, "google.protobuf.BoolValue"
21
+ optional :token, :message, 3, "google.protobuf.StringValue"
22
+ optional :trace_reporter_type, :enum, 5, "hypertrace.agent.config.v1.TraceReporterType"
23
+ optional :cert_file, :message, 6, "google.protobuf.StringValue"
24
+ optional :metric_endpoint, :message, 7, "google.protobuf.StringValue"
25
+ optional :metric_reporter_type, :enum, 8, "hypertrace.agent.config.v1.MetricReporterType"
26
+ end
27
+ add_message "hypertrace.agent.config.v1.Message" do
28
+ optional :request, :message, 1, "google.protobuf.BoolValue"
29
+ optional :response, :message, 2, "google.protobuf.BoolValue"
30
+ end
31
+ add_message "hypertrace.agent.config.v1.DataCapture" do
32
+ optional :http_headers, :message, 1, "hypertrace.agent.config.v1.Message"
33
+ optional :http_body, :message, 2, "hypertrace.agent.config.v1.Message"
34
+ optional :rpc_metadata, :message, 3, "hypertrace.agent.config.v1.Message"
35
+ optional :rpc_body, :message, 4, "hypertrace.agent.config.v1.Message"
36
+ optional :body_max_size_bytes, :message, 5, "google.protobuf.Int32Value"
37
+ optional :body_max_processing_size_bytes, :message, 6, "google.protobuf.Int32Value"
38
+ end
39
+ add_message "hypertrace.agent.config.v1.JavaAgent" do
40
+ repeated :filter_jar_paths, :message, 1, "google.protobuf.StringValue"
41
+ end
42
+ add_enum "hypertrace.agent.config.v1.PropagationFormat" do
43
+ value :B3, 0
44
+ value :TRACECONTEXT, 1
45
+ end
46
+ add_enum "hypertrace.agent.config.v1.TraceReporterType" do
47
+ value :UNSPECIFIED, 0
48
+ value :ZIPKIN, 1
49
+ value :OTLP, 2
50
+ value :LOGGING, 3
51
+ value :NONE, 4
52
+ end
53
+ add_enum "hypertrace.agent.config.v1.MetricReporterType" do
54
+ value :METRIC_REPORTER_TYPE_UNSPECIFIED, 0
55
+ value :METRIC_REPORTER_TYPE_OTLP, 1
56
+ value :METRIC_REPORTER_TYPE_PROMETHEUS, 2
57
+ value :METRIC_REPORTER_TYPE_LOGGING, 3
58
+ value :METRIC_REPORTER_TYPE_NONE, 4
59
+ end
60
+ end
61
+ end
62
+
63
+ module Hypertrace
64
+ module Agent
65
+ module Config
66
+ module V1
67
+ AgentConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.AgentConfig").msgclass
68
+ Reporting = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.Reporting").msgclass
69
+ Message = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.Message").msgclass
70
+ DataCapture = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.DataCapture").msgclass
71
+ JavaAgent = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.JavaAgent").msgclass
72
+ PropagationFormat = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.PropagationFormat").enummodule
73
+ TraceReporterType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.TraceReporterType").enummodule
74
+ MetricReporterType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hypertrace.agent.config.v1.MetricReporterType").enummodule
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ DEFAULT_AGENT_CONFIG = {
2
+ 'enabled': true,
3
+ 'propagation_formats': ['TRACECONTEXT'],
4
+ 'service_name': 'rubyagent',
5
+ 'reporting': {
6
+ 'endpoint': 'http://localhost:4318/v1/traces',
7
+ 'secure': false,
8
+ 'trace_reporter_type': 'OTLP',
9
+ 'token': '',
10
+ },
11
+ 'data_capture': {
12
+ 'http_headers': {
13
+ 'request': true,
14
+ 'response': true,
15
+ },
16
+ 'http_body': {
17
+ 'request': true,
18
+ 'response': true,
19
+ },
20
+ 'rpc_metadata': {
21
+ 'request': true,
22
+ 'response': true,
23
+ },
24
+ 'rpc_body': {
25
+ 'request': true,
26
+ 'response': true,
27
+ },
28
+ 'body_max_size_bytes': 131072
29
+ },
30
+ 'resource_attributes': {}
31
+ }.freeze
@@ -0,0 +1,142 @@
1
+ module Hypertrace::Config::Environment
2
+ class << self
3
+ include Hypertrace::Logging
4
+
5
+ def load_config_from_env
6
+ config = {}
7
+
8
+ service_name = Hypertrace::EnvVarSettings.env_value('SERVICE_NAME')
9
+ if service_name
10
+ log.debug "[env] Loaded SERVICE_NAME from env"
11
+ config[:service_name] = service_name
12
+ end
13
+
14
+ # Reporting
15
+ config[:reporting] = {}
16
+ reporting_endpoint = Hypertrace::EnvVarSettings.env_value('REPORTING_ENDPOINT')
17
+ if reporting_endpoint
18
+ log.debug "[env] Loaded REPORTING_ENDPOINT from env"
19
+ config[:reporting][:endpoint] = reporting_endpoint
20
+ end
21
+
22
+ reporter_type = Hypertrace::EnvVarSettings.env_value('REPORTING_TRACE_REPORTER_TYPE')
23
+ if reporter_type
24
+ log.debug "[env] Loaded REPORTING_TRACE_REPORTER_TYPE from env"
25
+ config[:reporting][:trace_reporter_type] = reporter_type
26
+ end
27
+
28
+ reporting_secure = Hypertrace::EnvVarSettings.env_value('REPORTING_SECURE')
29
+ if reporting_secure
30
+ log.debug "[env] Loaded REPORTING_SECURE from env"
31
+ config[:reporting][:secure] = is_true(reporting_secure)
32
+ end
33
+
34
+ reporting_token = Hypertrace::EnvVarSettings.env_value('REPORTING_TOKEN')
35
+ if reporting_token
36
+ log.debug("[env] Loaded REPORTING_TOKEN from env")
37
+ config[:reporting][:token] = reporting_token
38
+ end
39
+
40
+ config.delete(:reporting) if config[:reporting].empty?
41
+
42
+ # Data capture
43
+ config[:data_capture] = {}
44
+
45
+ config[:data_capture][:http_headers] = {}
46
+ headers_request = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_HTTP_HEADERS_REQUEST')
47
+ if headers_request
48
+ log.debug("[env] Loaded DATA_CAPTURE_HTTP_HEADERS_REQUEST from env")
49
+ config[:data_capture][:http_headers][:request] = is_true(headers_request)
50
+ end
51
+
52
+ headers_response = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_HTTP_HEADERS_RESPONSE')
53
+ if headers_response
54
+ log.debug("[env] Loaded DATA_CAPTURE_HTTP_HEADERS_RESPONSE from env")
55
+ config[:data_capture][:http_headers][:response] = is_true(headers_response)
56
+ end
57
+
58
+ config[:data_capture].delete(:http_headers) if config[:data_capture][:http_headers].empty?
59
+
60
+ config[:data_capture][:http_body] = {}
61
+ body_request = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_HTTP_BODY_REQUEST')
62
+ if body_request
63
+ log.debug("[env] Loaded DATA_CAPTURE_HTTP_BODY_REQUEST from env")
64
+ config[:data_capture][:http_body][:request] = is_true(body_request)
65
+ end
66
+
67
+ body_response = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_HTTP_BODY_RESPONSE')
68
+ if body_response
69
+ log.debug("[env] Loaded DATA_CAPTURE_HTTP_BODY_RESPONSE from env")
70
+ config[:data_capture][:http_body][:response] = is_true(body_response)
71
+ end
72
+
73
+ config[:data_capture].delete(:http_body) if config[:data_capture][:http_body].empty?
74
+
75
+ config[:data_capture][:rpc_metadata] = {}
76
+ rpc_metadata_request = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_RPC_METADATA_REQUEST')
77
+ if rpc_metadata_request
78
+ log.debug("[env] Loaded DATA_CAPTURE_RPC_METADATA_REQUEST from env")
79
+ config[:data_capture][:rpc_metadata][:request] = is_true(rpc_metadata_request)
80
+ end
81
+
82
+ rpc_metadata_response = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_RPC_METADATA_RESPONSE')
83
+ if rpc_metadata_response
84
+ log.debug("[env] Loaded DATA_CAPTURE_RPC_METADATA_RESPONSE from env")
85
+ config[:data_capture][:rpc_metadata][:response] = is_true(rpc_metadata_response)
86
+ end
87
+ config[:data_capture].delete(:rpc_metadata) if config[:data_capture][:rpc_metadata].empty?
88
+
89
+ config[:data_capture][:rpc_body] = {}
90
+ rpc_body_request = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_RPC_BODY_REQUEST')
91
+ if rpc_body_request
92
+ log.debug("[env] Loaded DATA_CAPTURE_RPC_BODY_REQUEST from env")
93
+ config[:data_capture][:rpc_body][:request] = is_true(rpc_body_request)
94
+ end
95
+
96
+ rpc_body_response = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_RPC_BODY_RESPONSE')
97
+ if rpc_body_response
98
+ log.debug("[env] Loaded DATA_CAPTURE_RPC_BODY_RESPONSE from env")
99
+ config[:data_capture][:rpc_body][:response] = is_true(rpc_body_response)
100
+ end
101
+ config[:data_capture].delete(:rpc_body) if config[:data_capture][:rpc_body].empty?
102
+
103
+ body_max_size_bytes = Hypertrace::EnvVarSettings.env_value('DATA_CAPTURE_BODY_MAX_SIZE_BYTES')
104
+ if body_max_size_bytes
105
+ log.debug("[env] Loaded DATA_CAPTURE_BODY_MAX_SIZE_BYTES from env")
106
+ config[:data_capture][:body_max_size_bytes] = body_max_size_bytes.to_i
107
+ end
108
+
109
+ config.delete(:data_capture) if config[:data_capture].empty?
110
+
111
+ propagation_formats = Hypertrace::EnvVarSettings.env_value(:propagation_formats)
112
+ if propagation_formats && propagation_formats.length > 0
113
+ log.debug("[env] Loaded PROPAGATION_FORMATS from env")
114
+ config[:propagation_formats] = propagation_formats.split(',')
115
+ end
116
+
117
+ enabled = Hypertrace::EnvVarSettings.env_value(:enabled)
118
+ if enabled
119
+ log.debug("[env] Loaded ENABLED from env")
120
+ config[:enabled] = is_true(enabled)
121
+ end
122
+
123
+ resource_attributes = Hypertrace::EnvVarSettings.env_value(:resource_attributes)
124
+ if resource_attributes
125
+ config[:resource_attributes] = {}
126
+ log.debug('[env] Loaded RESOURCE_ATTRIBUTES from env')
127
+ groups = resource_attributes.split(',')
128
+ groups.each do |group|
129
+ key, value = group.split('=')
130
+ config[:resource_attributes][key] = value
131
+ end
132
+ end
133
+
134
+ return config
135
+ end
136
+
137
+ def is_true value
138
+ value.downcase == 'true'
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hypertrace::Config
4
+ require_relative './config/default'
5
+ require_relative './config/environment'
6
+ require_relative './config/config_pb'
7
+ require_relative './config/config'
8
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hypertrace::EnvVarSettings
4
+ PREFIXES = ['HT']
5
+ class << self
6
+ def env_value target_key
7
+ PREFIXES.each do |prefix|
8
+ key = "#{prefix}_#{target_key}"
9
+ return ENV[key] if ENV.key?(key)
10
+ end
11
+ nil
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ class Hypertrace::Instrumentation::DataCapture
2
+ TYPE_REQUEST = 'request'
3
+ TYPE_RESPONSE = 'response'
4
+ CONTENT_TYPE_SUBSTRINGS = %w[json x-www-form-urlencoded]
5
+
6
+ def self.headers_to_attribute_keys header_hash, type, &block
7
+ return {} unless header_allowed_by_config?(type)
8
+
9
+ attrs = {}
10
+ header_hash.each do |header_key, header_value|
11
+ attr_key = "http.#{type}.header.#{header_key.downcase}"
12
+ header_value = header_value.join(',') if header_value.is_a?(Array)
13
+ if block_given?
14
+ yield attr_key, header_value
15
+ else
16
+ attrs[attr_key] = header_value
17
+ end
18
+ end
19
+ return if block_given?
20
+ return attrs
21
+ end
22
+
23
+ def self.capturable_body body_object
24
+ max_capture = Hypertrace::RubyAgent.config.data_capture.body_max_size_bytes.value
25
+ if body_object.is_a?(String)
26
+ return body_object.byteslice(0..max_capture)
27
+ elsif body_object.is_a?(StringIO)
28
+ result = body_object.read(max_capture)
29
+ body_object.rewind
30
+ return result
31
+ end
32
+ end
33
+
34
+ def self.can_capture?(content_type, type)
35
+ return false unless content_type
36
+ return false unless body_allowed_by_config?(type)
37
+
38
+ content_type = content_type.downcase
39
+ CONTENT_TYPE_SUBSTRINGS.each do |substring|
40
+ if content_type.include?(substring)
41
+ return true
42
+ end
43
+ end
44
+ false
45
+ end
46
+
47
+ def self.header_allowed_by_config? type
48
+ return Hypertrace::RubyAgent.config.data_capture.http_headers.request.value if type == TYPE_REQUEST
49
+ Hypertrace::RubyAgent.config.data_capture.http_headers.response.value
50
+ end
51
+
52
+ def self.body_allowed_by_config? type
53
+ return Hypertrace::RubyAgent.config.data_capture.http_body.request.value if type == TYPE_REQUEST
54
+ Hypertrace::RubyAgent.config.data_capture.http_body.response.value
55
+ end
56
+
57
+ end
@@ -0,0 +1,40 @@
1
+ class OpenTelemetry::Instrumentation::Faraday::Middlewares::TracerMiddleware < ::Faraday::Middleware
2
+ def call(env)
3
+ http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method]
4
+ attributes = span_creation_attributes(
5
+ http_method: http_method, url: env.url
6
+ )
7
+ request_headers = env.request_headers.to_h
8
+ header_attrs = Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(request_headers,
9
+ Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
10
+
11
+ content_type = request_headers['Content-Type']
12
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
13
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(env&.request_body&.to_s)
14
+ attributes['http.request.body'] = body_cap if body_cap
15
+ end
16
+
17
+ attributes.merge!(header_attrs)
18
+ tracer.in_span(
19
+ "HTTP #{http_method}", attributes: attributes, kind: :client
20
+ ) do |span|
21
+ OpenTelemetry.propagation.inject(env.request_headers)
22
+
23
+ app.call(env).on_complete do |resp|
24
+ resp = Faraday::Response.new(resp)
25
+ resp_headers = resp.headers.to_h
26
+ Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(resp_headers,
27
+ Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE) do |k, v|
28
+ span.set_attribute(k, v)
29
+ end
30
+ content_type = resp_headers['content-type']
31
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
32
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(resp.body)
33
+ span.set_attribute('http.response.body', body_cap) if body_cap
34
+ end
35
+
36
+ trace_response(span, resp)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ module OpenTelemetry::Instrumentation::HTTP::Patches::Client
2
+ def perform(req, options)
3
+ uri = req.uri
4
+ request_method = req.verb.to_s.upcase
5
+
6
+ headers = req.headers.to_h
7
+ attrs = Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(headers,
8
+ Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
9
+ content_type = headers['Content-Type']
10
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
11
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(req.body.source)
12
+ attrs['http.request.body'] = body_cap if body_cap
13
+ end
14
+
15
+ attributes = {
16
+ 'http.method' => request_method,
17
+ 'http.scheme' => uri.scheme,
18
+ 'http.target' => uri.path,
19
+ 'http.url' => uri.to_s,
20
+ 'net.peer.name' => uri.host,
21
+ 'net.peer.port' => uri.port
22
+ }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes).merge!(attrs)
23
+
24
+ tracer.in_span("HTTP #{request_method}", attributes: attributes, kind: :client) do |span|
25
+ OpenTelemetry.propagation.inject(req.headers)
26
+ super.tap do |response|
27
+ response_headers = response.headers.to_h
28
+ Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(response_headers,
29
+ Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE) do |k, v|
30
+ span.set_attribute(k, v)
31
+ end
32
+ content_type = response_headers['Content-Type']
33
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
34
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(response.body.to_s)
35
+ span.set_attribute('http.response.body', body_cap) if body_cap
36
+ end
37
+ annotate_span_with_response!(span, response)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ module OpenTelemetry::Instrumentation::Net::HTTP::Patches::Instrumentation
2
+ def request(req, body = nil, &block)
3
+ # Do not trace recursive call for starting the connection
4
+ return super(req, body, &block) unless started?
5
+
6
+ scheme = use_ssl? ? "https://" : "http"
7
+ url = "#{scheme}#{@address}:#{@port}#{req.path}"
8
+ attributes = {
9
+ OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => req.method,
10
+ OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => USE_SSL_TO_SCHEME[use_ssl?],
11
+ OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => req.path,
12
+ OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => @address,
13
+ OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => @port,
14
+ OpenTelemetry::SemanticConventions::Trace::HTTP_URL => url
15
+ }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
16
+
17
+ header_map = req.instance_variable_get(:@header)
18
+ ht_attributes = Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(header_map,
19
+ Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
20
+ content_type = header_map['content-type']&.first
21
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
22
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(req.body)
23
+ ht_attributes['http.request.body'] = body_cap if body_cap
24
+ end
25
+ tracer.in_span(
26
+ HTTP_METHODS_TO_SPAN_NAMES[req.method],
27
+ attributes: attributes.merge!(ht_attributes),
28
+ kind: :client
29
+ ) do |span|
30
+ OpenTelemetry.propagation.inject(req)
31
+
32
+ super(req, body, &block).tap do |response|
33
+ response_headers = response.instance_variable_get(:@header)
34
+ Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(response_headers,
35
+ Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE) do |k, v|
36
+ span.set_attribute(k, v)
37
+ end
38
+ content_type = response_headers['content-type']&.first
39
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
40
+ span.set_attribute('http.response.body', Hypertrace::Instrumentation::DataCapture.capturable_body(response.body))
41
+ end
42
+ annotate_span_with_response!(span, response)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OpenTelemetry::Instrumentation::Rack::Middlewares::TracerMiddleware
4
+ def call(env)
5
+ if untraced_request?(env)
6
+ OpenTelemetry::Common::Utilities.untraced do
7
+ return @app.call(env)
8
+ end
9
+ end
10
+
11
+ original_env = env.dup
12
+ extracted_context = OpenTelemetry.propagation.extract(
13
+ env,
14
+ getter: OpenTelemetry::Common::Propagation.rack_env_getter
15
+ )
16
+ frontend_context = create_frontend_span(env, extracted_context)
17
+
18
+ # restore extracted context in this process:
19
+ OpenTelemetry::Context.with_current(frontend_context || extracted_context) do
20
+ request_span_name = create_request_span_name(env['REQUEST_URI'] || original_env['PATH_INFO'], env)
21
+ request_span_kind = frontend_context.nil? ? :server : :internal
22
+
23
+ ht_attributes = Hypertrace::Instrumentation::RackCompatible.extract_req_headers_as_attributes(env, 'http.request.header')
24
+ content_type = env['CONTENT_TYPE']
25
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type,
26
+ Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
27
+
28
+ body_cap = Hypertrace::Instrumentation::DataCapture.capturable_body(env['rack.input'])
29
+ ht_attributes['http.request.body'] = body_cap if body_cap
30
+ end
31
+
32
+ tracer.in_span(request_span_name,
33
+ attributes: request_span_attributes(env: env).merge(ht_attributes),
34
+ kind: request_span_kind) do |request_span|
35
+ OpenTelemetry::Instrumentation::Rack.with_span(request_span) do
36
+ @app.call(env).tap do |status, headers, response|
37
+
38
+ response_headers = headers.to_hash
39
+ Hypertrace::Instrumentation::DataCapture.headers_to_attribute_keys(response_headers, Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
40
+ response_headers.each do |h_k, h_v|
41
+ request_span.set_attribute("http.response.header.#{h_k.downcase}", h_v)
42
+ end
43
+ content_type_arr = response_headers.find{|x|x[0].downcase == "content-type"}
44
+ if Hypertrace::Instrumentation::DataCapture.can_capture?(content_type_arr&.join(''),
45
+ Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
46
+ cap_body = Hypertrace::Instrumentation::RackCompatible.extract_response_body(response)
47
+ request_span.set_attribute('http.response.body', cap_body) if cap_body
48
+ end
49
+
50
+ set_attributes_after_request(request_span, status, headers, response)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ ensure
56
+ finish_span(frontend_context)
57
+ end
58
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hypertrace
4
+ module Instrumentation
5
+ class RackCompatible
6
+
7
+ CONTENT_TYPE_SUBSTRINGS = %w[json x-www-form-urlencoded]
8
+
9
+ def self.extract_req_headers_as_attributes env, attr_prefix
10
+ return unless Hypertrace::RubyAgent.instance.config.data_capture.http_headers.request
11
+
12
+ headers = extract_headers_from_env(env)
13
+ attr_hash = headers.map do |key, value|
14
+ # https://github.com/rails/rails/blob/a6bf6d55804850b390d840b87a679b784c0a3db4/actionpack/lib/action_dispatch/http/headers.rb#L5-L23
15
+ # in rack underscore and dashes resolve to the same header(using - instead of _)
16
+ key = key.gsub('_', '-')
17
+ ["#{attr_prefix}.#{key}".downcase, value]
18
+ end.to_h
19
+
20
+ attr_hash
21
+ end
22
+
23
+ def self.should_record_env?(env)
24
+ return false unless env.has_key?("CONTENT_TYPE")
25
+ Hypertrace::Instrumentation::DataCapture.can_capture?(env['CONTENT_TYPE'], Hypertrace::Instrumentation::DataCapture::TYPE_REQUEST)
26
+ end
27
+
28
+ def self.should_record_rack_array?(response)
29
+ content_type_arr = response[1].find{|x|x && x[0].downcase == "content-type"}
30
+ return false unless content_type_arr
31
+ content_type = content_type_arr[1]
32
+ Hypertrace::Instrumentation::DataCapture.can_capture?(content_type, Hypertrace::Instrumentation::DataCapture::TYPE_RESPONSE)
33
+ end
34
+
35
+ def self.extract_response_body rack_response
36
+ if rack_response.is_a?(Rack::BodyProxy)
37
+ Hypertrace::Instrumentation::DataCapture.capturable_body(rack_response.join(''))
38
+ elsif rack_response.is_a?(Array) && rack_response.length == 3
39
+ Hypertrace::Instrumentation::DataCapture.capturable_body(rack_response[2].join(''))
40
+ end
41
+ end
42
+
43
+ def self.extract_response_headers_as_attributes rack_response, attr_prefix
44
+ return unless Hypertrace::RubyAgent.instance.config.data_capture.http_headers.response
45
+ return if rack_response.is_a?(Rack::BodyProxy)
46
+
47
+ rack_response[1].map do |header, value|
48
+ ["#{attr_prefix}.#{header}".downcase, value]
49
+ end.to_h
50
+ end
51
+
52
+ def self.extract_headers_from_env env
53
+ result = env.select{|x|x.start_with?("HTTP_")}
54
+ result.transform_keys!{|k|k[5..-1]}
55
+ if env.has_key?("CONTENT_TYPE")
56
+ result['content-type'] = env['CONTENT_TYPE']
57
+ end
58
+ if env.has_key?("CONTENT_LENGTH")
59
+ result['content-length'] = env["CONTENT_LENGTH"]
60
+ end
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end