hypertrace-agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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