instana 2.7.2 → 2.8.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.
- checksums.yaml +4 -4
- data/lib/instana/backend/gc_snapshot.rb +4 -4
- data/lib/instana/backend/host_agent_reporting_observer.rb +60 -5
- data/lib/instana/config.rb +152 -12
- data/lib/instana/exporter/otlp/aws_converter.rb +148 -0
- data/lib/instana/exporter/otlp/background_job_converter.rb +78 -0
- data/lib/instana/exporter/otlp/base_converter.rb +418 -0
- data/lib/instana/exporter/otlp/converter_factory.rb +135 -0
- data/lib/instana/exporter/otlp/custom_converter.rb +42 -0
- data/lib/instana/exporter/otlp/database_converter.rb +153 -0
- data/lib/instana/exporter/otlp/graphql_converter.rb +66 -0
- data/lib/instana/exporter/otlp/http_converter.rb +129 -0
- data/lib/instana/exporter/otlp/messaging_converter.rb +81 -0
- data/lib/instana/exporter/otlp/rails_converter.rb +114 -0
- data/lib/instana/exporter/otlp/resource.rb +403 -0
- data/lib/instana/exporter/otlp/rpc_converter.rb +102 -0
- data/lib/instana/version.rb +1 -1
- metadata +42 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/db'
|
|
7
|
+
require 'opentelemetry/semconv/server'
|
|
8
|
+
|
|
9
|
+
module Instana
|
|
10
|
+
module Exporter
|
|
11
|
+
module Otlp
|
|
12
|
+
# Converter for database spans to OTLP format
|
|
13
|
+
class DatabaseConverter < BaseConverter
|
|
14
|
+
def convert_attributes
|
|
15
|
+
attributes = {}
|
|
16
|
+
data = span[:data] || {}
|
|
17
|
+
|
|
18
|
+
convert_activerecord_attributes(attributes, data[:activerecord])
|
|
19
|
+
convert_sequel_attributes(attributes, data[:sequel])
|
|
20
|
+
convert_redis_attributes(attributes, data[:redis])
|
|
21
|
+
convert_memcache_attributes(attributes, data[:memcache])
|
|
22
|
+
convert_mongo_attributes(attributes, data[:mongo])
|
|
23
|
+
|
|
24
|
+
attributes
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Build OTel-compliant span name for database spans
|
|
28
|
+
#
|
|
29
|
+
# Formulas per SPAN_NAME_PATTERNS.txt Section 2:
|
|
30
|
+
# activerecord / sequel → "{adapter} {db}" e.g. "mysql2 myapp"
|
|
31
|
+
# redis → "redis {command}" e.g. "redis GET"
|
|
32
|
+
# memcache → "memcached {command}" e.g. "memcached get"
|
|
33
|
+
# mongo → "{namespace}.{command}" e.g. "users.find"
|
|
34
|
+
#
|
|
35
|
+
# @return [String] The span name
|
|
36
|
+
def span_name
|
|
37
|
+
data = span[:data] || {}
|
|
38
|
+
activerecord_span_name(data[:activerecord]) ||
|
|
39
|
+
sequel_span_name(data[:sequel]) ||
|
|
40
|
+
redis_span_name(data[:redis]) ||
|
|
41
|
+
memcache_span_name(data[:memcache]) ||
|
|
42
|
+
mongo_span_name(data[:mongo]) ||
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def activerecord_span_name(ar_data)
|
|
49
|
+
return unless ar_data
|
|
50
|
+
|
|
51
|
+
parts = [ar_data[:adapter], ar_data[:db]].compact.reject(&:empty?)
|
|
52
|
+
parts.empty? ? 'activerecord' : parts.join(' ')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def sequel_span_name(seq)
|
|
56
|
+
return unless seq
|
|
57
|
+
|
|
58
|
+
parts = [seq[:adapter], seq[:db]].compact.reject(&:empty?)
|
|
59
|
+
parts.empty? ? 'sequel' : parts.join(' ')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def redis_span_name(redis)
|
|
63
|
+
return unless redis
|
|
64
|
+
|
|
65
|
+
cmd = redis[:command].to_s.strip
|
|
66
|
+
cmd.empty? ? 'redis' : "redis #{cmd}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def memcache_span_name(mc_data)
|
|
70
|
+
return unless mc_data
|
|
71
|
+
|
|
72
|
+
cmd = mc_data[:command].to_s.strip
|
|
73
|
+
cmd.empty? ? 'memcached' : "memcached #{cmd}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def mongo_span_name(mongo)
|
|
77
|
+
return unless mongo
|
|
78
|
+
|
|
79
|
+
ns = mongo[:namespace].to_s.strip
|
|
80
|
+
cmd = mongo[:command].to_s.strip
|
|
81
|
+
parts = [ns, cmd].reject(&:empty?)
|
|
82
|
+
parts.empty? ? 'mongodb' : parts.join('.')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def convert_activerecord_attributes(attributes, ar_data)
|
|
86
|
+
return unless ar_data
|
|
87
|
+
|
|
88
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, ar_data[:adapter])
|
|
89
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, ar_data[:db])
|
|
90
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, ar_data[:sql])
|
|
91
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, ar_data[:username])
|
|
92
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, ar_data[:host])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def convert_sequel_attributes(attributes, seq_data)
|
|
96
|
+
return unless seq_data
|
|
97
|
+
|
|
98
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, seq_data[:adapter])
|
|
99
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, seq_data[:db])
|
|
100
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, seq_data[:sql])
|
|
101
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, seq_data[:username])
|
|
102
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, seq_data[:host])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def convert_redis_attributes(attributes, redis_data)
|
|
106
|
+
return unless redis_data
|
|
107
|
+
|
|
108
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'redis')
|
|
109
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, redis_data[:command])
|
|
110
|
+
add_attribute(attributes, 'db.redis.database_index', redis_data[:db])
|
|
111
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(redis_data[:connection]))
|
|
112
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(redis_data[:connection]))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def convert_memcache_attributes(attributes, mc_data)
|
|
116
|
+
return unless mc_data
|
|
117
|
+
|
|
118
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'memcached')
|
|
119
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mc_data[:command])
|
|
120
|
+
add_attribute(attributes, 'db.memcached.key', mc_data[:key])
|
|
121
|
+
add_attribute(attributes, 'db.memcached.keys', mc_data[:keys])
|
|
122
|
+
add_attribute(attributes, 'db.memcached.namespace', mc_data[:namespace])
|
|
123
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(mc_data[:server]))
|
|
124
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(mc_data[:server]))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def convert_mongo_attributes(attributes, mongo_data)
|
|
128
|
+
return unless mongo_data
|
|
129
|
+
|
|
130
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'mongodb')
|
|
131
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, mongo_data[:namespace])
|
|
132
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mongo_data[:command])
|
|
133
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, mongo_data[:json])
|
|
134
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, mongo_data.dig(:peer, :hostname))
|
|
135
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, mongo_data.dig(:peer, :port))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def extract_host(connection)
|
|
139
|
+
return nil unless connection
|
|
140
|
+
|
|
141
|
+
connection.to_s.split(':').first
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_port(connection)
|
|
145
|
+
return nil unless connection
|
|
146
|
+
|
|
147
|
+
port = connection.to_s.split(':').last
|
|
148
|
+
port.to_i if port =~ /^\d+$/
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/incubating/graphql'
|
|
7
|
+
|
|
8
|
+
module Instana
|
|
9
|
+
module Exporter
|
|
10
|
+
module Otlp
|
|
11
|
+
# Converter for GraphQL spans to OTLP format
|
|
12
|
+
class GraphqlConverter < BaseConverter
|
|
13
|
+
# Build OTel-compliant span name for GraphQL spans
|
|
14
|
+
#
|
|
15
|
+
# Formula per SPAN_NAME_PATTERNS.txt Section 7 (observability):
|
|
16
|
+
# "{operationType} {operationName}" e.g. "query MyQuery"
|
|
17
|
+
# Falls back to just "{operationType}" when no name, or "graphql" when both absent.
|
|
18
|
+
#
|
|
19
|
+
# @return [String] The span name
|
|
20
|
+
def span_name
|
|
21
|
+
gql = span[:data]&.[](:graphql) || {}
|
|
22
|
+
type = gql[:operationType].to_s.strip
|
|
23
|
+
name = gql[:operationName].to_s.strip
|
|
24
|
+
|
|
25
|
+
if type.empty?
|
|
26
|
+
'graphql'
|
|
27
|
+
elsif name.empty?
|
|
28
|
+
type
|
|
29
|
+
else
|
|
30
|
+
"#{type} #{name}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def convert_attributes
|
|
35
|
+
attributes = {}
|
|
36
|
+
|
|
37
|
+
graphql_data = span[:data]&.[](:graphql)
|
|
38
|
+
return attributes unless graphql_data
|
|
39
|
+
|
|
40
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::GRAPHQL::GRAPHQL_OPERATION_NAME, graphql_data[:operationName])
|
|
41
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::GRAPHQL::GRAPHQL_OPERATION_TYPE, graphql_data[:operationType])
|
|
42
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::GRAPHQL::GRAPHQL_DOCUMENT, format_fields(graphql_data[:fields]))
|
|
43
|
+
|
|
44
|
+
# Add arguments as custom attribute
|
|
45
|
+
add_attribute(attributes, 'graphql.arguments', format_arguments(graphql_data[:arguments])) if graphql_data[:arguments]
|
|
46
|
+
|
|
47
|
+
attributes
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def format_fields(fields)
|
|
53
|
+
return nil unless fields
|
|
54
|
+
|
|
55
|
+
fields.map { |obj, flds| "#{obj} { #{flds.join(', ')} }" }.join(', ')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_arguments(arguments)
|
|
59
|
+
return nil unless arguments
|
|
60
|
+
|
|
61
|
+
arguments.map { |obj, args| "#{obj}(#{args.join(', ')})" }.join(', ')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/http'
|
|
7
|
+
require 'opentelemetry/semconv/url'
|
|
8
|
+
require 'opentelemetry/semconv/server'
|
|
9
|
+
require 'opentelemetry/semconv/user_agent'
|
|
10
|
+
|
|
11
|
+
module Instana
|
|
12
|
+
module Exporter
|
|
13
|
+
module Otlp
|
|
14
|
+
# Converter for HTTP spans to OTLP format
|
|
15
|
+
# Handles conversion of HTTP-related spans with specific attributes
|
|
16
|
+
class HttpConverter < BaseConverter
|
|
17
|
+
# Extract HTTP-specific attributes as plain key/value pairs
|
|
18
|
+
# @return [Hash] HTTP attributes
|
|
19
|
+
def convert_attributes
|
|
20
|
+
attributes = {}
|
|
21
|
+
http_data = span[:data]&.[](:http) || {}
|
|
22
|
+
|
|
23
|
+
add_attribute(attributes, OpenTelemetry::SemConv::HTTP::HTTP_REQUEST_METHOD, http_data[:method])
|
|
24
|
+
add_attribute(attributes, OpenTelemetry::SemConv::URL::URL_FULL, http_data[:url])
|
|
25
|
+
add_attribute(attributes, OpenTelemetry::SemConv::URL::URL_PATH, http_data[:path])
|
|
26
|
+
add_attribute(attributes, OpenTelemetry::SemConv::URL::URL_QUERY, http_data[:params])
|
|
27
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(http_data[:host]))
|
|
28
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(http_data[:host], http_data[:url]))
|
|
29
|
+
add_attribute(attributes, OpenTelemetry::SemConv::URL::URL_SCHEME, extract_scheme(http_data[:url]))
|
|
30
|
+
add_attribute(attributes, OpenTelemetry::SemConv::HTTP::HTTP_RESPONSE_STATUS_CODE, http_data[:status])
|
|
31
|
+
add_attribute(attributes, OpenTelemetry::SemConv::USER_AGENT::USER_AGENT_ORIGINAL, http_data.dig(:header, 'user-agent'))
|
|
32
|
+
|
|
33
|
+
add_protocol_attributes(attributes, http_data[:protocol])
|
|
34
|
+
|
|
35
|
+
attributes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build OTel-compliant span status, treating EXIT spans with 4xx as ERROR
|
|
39
|
+
#
|
|
40
|
+
# @param error_count [Integer] Span error count
|
|
41
|
+
# @param error_msg [String, nil] Pre-extracted error message
|
|
42
|
+
# @return [Status]
|
|
43
|
+
def build_status(error_count, error_msg)
|
|
44
|
+
return super unless error_count.zero?
|
|
45
|
+
|
|
46
|
+
http_data = span[:data]&.[](:http) || {}
|
|
47
|
+
status_code = http_data[:status].to_i
|
|
48
|
+
|
|
49
|
+
if convert_span_kind == :client && status_code >= 400 && status_code < 500
|
|
50
|
+
Status.new(OpenTelemetry::Trace::Status::ERROR, '')
|
|
51
|
+
else
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build OTel-compliant span name for HTTP spans
|
|
57
|
+
#
|
|
58
|
+
# Convention (stable): "{METHOD}" or "{METHOD} {url.template/path}"
|
|
59
|
+
# Falls back to "HTTP" when no method is present.
|
|
60
|
+
#
|
|
61
|
+
# @return [String] The span name
|
|
62
|
+
def span_name
|
|
63
|
+
http_data = span[:data]&.[](:http) || {}
|
|
64
|
+
method = http_data[:method].to_s.upcase
|
|
65
|
+
method = 'HTTP' if method.empty?
|
|
66
|
+
|
|
67
|
+
path = http_data[:path].to_s.strip
|
|
68
|
+
path.empty? ? method : "#{method} #{path}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Extract scheme from URL
|
|
74
|
+
# @param url [String] The URL
|
|
75
|
+
# @return [String, nil] The scheme (http or https)
|
|
76
|
+
def extract_scheme(url)
|
|
77
|
+
return nil unless url
|
|
78
|
+
|
|
79
|
+
uri = URI.parse(url)
|
|
80
|
+
uri.scheme
|
|
81
|
+
rescue URI::InvalidURIError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Extract the host part from a "host:port" string or bare hostname
|
|
86
|
+
# @param host_str [String, nil] e.g. "api.example.com:8080" or "api.example.com"
|
|
87
|
+
# @return [String, nil]
|
|
88
|
+
def extract_host(host_str)
|
|
89
|
+
return nil unless host_str
|
|
90
|
+
|
|
91
|
+
part = host_str.to_s.split(':').first
|
|
92
|
+
part && !part.empty? ? part : host_str
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Extract the port from a "host:port" string, falling back to the URL port
|
|
96
|
+
# @param host_str [String, nil] e.g. "api.example.com:8080"
|
|
97
|
+
# @param url [String, nil] full URL as fallback
|
|
98
|
+
# @return [Integer, nil]
|
|
99
|
+
def extract_port(host_str, url)
|
|
100
|
+
if host_str&.include?(':')
|
|
101
|
+
port = host_str.split(':').last
|
|
102
|
+
return port.to_i unless port.nil? || port.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
return nil unless url
|
|
106
|
+
|
|
107
|
+
uri = URI.parse(url)
|
|
108
|
+
uri.port
|
|
109
|
+
rescue URI::InvalidURIError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Emit network.protocol.name and network.protocol.version from e.g. "HTTP/1.1"
|
|
114
|
+
# @param attributes [Hash]
|
|
115
|
+
# @param protocol [String, nil] e.g. "HTTP/1.1" or "h2"
|
|
116
|
+
def add_protocol_attributes(attributes, protocol)
|
|
117
|
+
return unless protocol
|
|
118
|
+
|
|
119
|
+
parts = protocol.to_s.split('/', 2)
|
|
120
|
+
name = parts[0].downcase
|
|
121
|
+
version = parts[1]
|
|
122
|
+
|
|
123
|
+
add_attribute(attributes, 'network.protocol.name', name)
|
|
124
|
+
add_attribute(attributes, 'network.protocol.version', version)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/incubating/messaging'
|
|
7
|
+
require 'opentelemetry/semconv/server'
|
|
8
|
+
|
|
9
|
+
module Instana
|
|
10
|
+
module Exporter
|
|
11
|
+
module Otlp
|
|
12
|
+
# Converter for messaging spans to OTLP format
|
|
13
|
+
class MessagingConverter < BaseConverter
|
|
14
|
+
# Build OTel-compliant span name for messaging (RabbitMQ) spans
|
|
15
|
+
#
|
|
16
|
+
# Formula per SPAN_NAME_PATTERNS.txt Section 3 (bunny/AMQP):
|
|
17
|
+
# publish → "{exchange} publish" (or "{queue} publish" when no exchange)
|
|
18
|
+
# receive → "{queue} receive"
|
|
19
|
+
#
|
|
20
|
+
# @return [String] The span name
|
|
21
|
+
def span_name
|
|
22
|
+
rabbitmq_data = span[:data]&.[](:rabbitmq) || {}
|
|
23
|
+
sort = rabbitmq_data[:sort].to_s
|
|
24
|
+
|
|
25
|
+
if sort == 'publish'
|
|
26
|
+
dest = rabbitmq_data[:exchange].to_s.strip
|
|
27
|
+
dest = rabbitmq_data[:queue].to_s.strip if dest.empty?
|
|
28
|
+
dest.empty? ? 'publish' : "#{dest} publish"
|
|
29
|
+
else
|
|
30
|
+
queue = rabbitmq_data[:queue].to_s.strip
|
|
31
|
+
queue.empty? ? 'receive' : "#{queue} receive"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def convert_attributes
|
|
36
|
+
attributes = {}
|
|
37
|
+
|
|
38
|
+
rabbitmq_data = span[:data]&.[](:rabbitmq)
|
|
39
|
+
if rabbitmq_data
|
|
40
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'rabbitmq')
|
|
41
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME,
|
|
42
|
+
rabbitmq_destination_name(rabbitmq_data))
|
|
43
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, rabbitmq_data[:key])
|
|
44
|
+
add_attribute(attributes, 'messaging.rabbitmq.queue', rabbitmq_data[:queue])
|
|
45
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rabbitmq_data[:address])
|
|
46
|
+
|
|
47
|
+
operation = rabbitmq_data[:sort] == 'publish' ? 'send' : 'receive'
|
|
48
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, operation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attributes
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Build the composite destination name per spec:
|
|
57
|
+
# Producer (publish): "{exchange}:{key}" — omit absent parts
|
|
58
|
+
# Consumer (receive): "{exchange}:{key}:{queue}" — omit absent; deduplicate key==queue
|
|
59
|
+
#
|
|
60
|
+
# @param data [Hash] rabbitmq span data
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def rabbitmq_destination_name(data)
|
|
63
|
+
exchange = data[:exchange].to_s.strip
|
|
64
|
+
key = data[:key].to_s.strip
|
|
65
|
+
queue = data[:queue].to_s.strip
|
|
66
|
+
sort = data[:sort].to_s
|
|
67
|
+
|
|
68
|
+
if sort == 'publish'
|
|
69
|
+
parts = [exchange, key].reject(&:empty?)
|
|
70
|
+
else
|
|
71
|
+
# Consumer: exchange:key:queue, dedup key==queue
|
|
72
|
+
parts = [exchange, key]
|
|
73
|
+
parts << queue unless queue.empty? || queue == key
|
|
74
|
+
parts = parts.reject(&:empty?)
|
|
75
|
+
end
|
|
76
|
+
parts.empty? ? nil : parts.join(':')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/incubating/code'
|
|
7
|
+
|
|
8
|
+
module Instana
|
|
9
|
+
module Exporter
|
|
10
|
+
module Otlp
|
|
11
|
+
# Converter for Rails-related spans (ActionController, ActionView, ActionMailer) to OTLP format
|
|
12
|
+
class RailsConverter < BaseConverter
|
|
13
|
+
ACTIONMAILER_SPAN = 'mail.actionmailer'
|
|
14
|
+
|
|
15
|
+
# Build OTel-compliant span name for Rails spans
|
|
16
|
+
#
|
|
17
|
+
# Formulas per SPAN_NAME_PATTERNS.txt Sections 5 & 7:
|
|
18
|
+
# actioncontroller → "{Controller}#{action}" e.g. "UsersController#index"
|
|
19
|
+
# actionview → "{view_name}" e.g. "users/index"
|
|
20
|
+
# render → "{type} {name}" e.g. "template users/index"
|
|
21
|
+
# mail.actionmailer → "{Class}#{method}" e.g. "UserMailer#welcome_email"
|
|
22
|
+
#
|
|
23
|
+
# @return [String] The span name
|
|
24
|
+
def span_name
|
|
25
|
+
case span[:n].to_s
|
|
26
|
+
when 'actioncontroller'
|
|
27
|
+
d = span_data_for(:actioncontroller)
|
|
28
|
+
ctrl = d[:controller].to_s.strip
|
|
29
|
+
action = d[:action].to_s.strip
|
|
30
|
+
parts = [ctrl, action].reject(&:empty?)
|
|
31
|
+
parts.empty? ? 'actioncontroller' : parts.join('#')
|
|
32
|
+
when 'actionview'
|
|
33
|
+
d = span_data_for(:actionview)
|
|
34
|
+
d[:name].to_s.strip.then { |n| n.empty? ? 'actionview' : n }
|
|
35
|
+
when 'render'
|
|
36
|
+
d = span_data_for(:render)
|
|
37
|
+
type = d[:type].to_s.strip
|
|
38
|
+
name = d[:name].to_s.strip
|
|
39
|
+
parts = [type, name].reject(&:empty?)
|
|
40
|
+
parts.empty? ? 'render' : parts.join(' ')
|
|
41
|
+
when ACTIONMAILER_SPAN
|
|
42
|
+
d = span_data_for(:actionmailer)
|
|
43
|
+
klass = d[:class].to_s.strip
|
|
44
|
+
method = d[:method].to_s.strip
|
|
45
|
+
parts = [klass, method].reject(&:empty?)
|
|
46
|
+
parts.empty? ? ACTIONMAILER_SPAN : parts.join('#')
|
|
47
|
+
else
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def convert_attributes
|
|
53
|
+
attributes = {}
|
|
54
|
+
span_type = span[:n].to_s
|
|
55
|
+
|
|
56
|
+
if span_type == 'actioncontroller' # rubocop:disable Style/CaseLikeIf
|
|
57
|
+
convert_action_controller_attributes(attributes)
|
|
58
|
+
elsif span_type == 'actionview'
|
|
59
|
+
convert_action_view_attributes(attributes)
|
|
60
|
+
elsif span_type == 'render'
|
|
61
|
+
convert_render_attributes(attributes)
|
|
62
|
+
elsif span_type == ACTIONMAILER_SPAN
|
|
63
|
+
convert_action_mailer_attributes(attributes)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
attributes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Return the data hash for a given span data key, falling back to
|
|
72
|
+
# top-level span key, then an empty hash.
|
|
73
|
+
def span_data_for(key)
|
|
74
|
+
span[:data]&.[](key) || span[key] || {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Convert ActionController span attributes
|
|
78
|
+
def convert_action_controller_attributes(attributes)
|
|
79
|
+
controller_data = span[:data]&.[](:actioncontroller) || span[:actioncontroller]
|
|
80
|
+
return unless controller_data
|
|
81
|
+
|
|
82
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CODE::CODE_NAMESPACE, controller_data[:controller])
|
|
83
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CODE::CODE_FUNCTION, controller_data[:action])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Convert ActionView span attributes
|
|
87
|
+
def convert_action_view_attributes(attributes)
|
|
88
|
+
view_data = span[:data]&.[](:actionview) || span[:actionview]
|
|
89
|
+
return unless view_data
|
|
90
|
+
|
|
91
|
+
add_attribute(attributes, 'rails.view.name', view_data[:name])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert render span attributes
|
|
95
|
+
def convert_render_attributes(attributes)
|
|
96
|
+
render_data = span[:data]&.[](:render) || span[:render]
|
|
97
|
+
return unless render_data
|
|
98
|
+
|
|
99
|
+
add_attribute(attributes, 'rails.render.type', render_data[:type])
|
|
100
|
+
add_attribute(attributes, 'rails.render.name', render_data[:name])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Convert ActionMailer span attributes
|
|
104
|
+
def convert_action_mailer_attributes(attributes)
|
|
105
|
+
mailer_data = span[:data]&.[](:actionmailer) || span[:actionmailer]
|
|
106
|
+
return unless mailer_data
|
|
107
|
+
|
|
108
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CODE::CODE_NAMESPACE, mailer_data[:class])
|
|
109
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CODE::CODE_FUNCTION, mailer_data[:method])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|