newrelic_rpm 10.5.0 → 10.6.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/new_relic/agent/configuration/default_source.rb +4 -4
  4. data/lib/new_relic/agent/configuration/manager.rb +7 -0
  5. data/lib/new_relic/agent/database/explain_plan_helpers.rb +1 -1
  6. data/lib/new_relic/agent/error_collector.rb +2 -2
  7. data/lib/new_relic/agent/log_event_aggregator.rb +13 -5
  8. data/lib/new_relic/agent/opentelemetry/abstract_segment_patch.rb +47 -0
  9. data/lib/new_relic/agent/opentelemetry/attribute_translator.rb +10 -7
  10. data/lib/new_relic/agent/opentelemetry/messaging_patch.rb +39 -0
  11. data/lib/new_relic/agent/opentelemetry/span_event_primitive_patch.rb +115 -0
  12. data/lib/new_relic/agent/opentelemetry/trace/span.rb +23 -1
  13. data/lib/new_relic/agent/opentelemetry/trace/tracer.rb +45 -5
  14. data/lib/new_relic/agent/opentelemetry/transaction_patch.rb +20 -0
  15. data/lib/new_relic/agent/opentelemetry/translators/attribute_mappings.rb +127 -0
  16. data/lib/new_relic/agent/opentelemetry/translators/base_translator.rb +5 -5
  17. data/lib/new_relic/agent/opentelemetry/translators/datastore_translator.rb +2 -2
  18. data/lib/new_relic/agent/opentelemetry/translators/generic_translator.rb +1 -1
  19. data/lib/new_relic/agent/opentelemetry/translators/http_client_translator.rb +2 -2
  20. data/lib/new_relic/agent/opentelemetry/translators/http_server_translator.rb +2 -2
  21. data/lib/new_relic/agent/opentelemetry/translators/messaging_translator.rb +128 -0
  22. data/lib/new_relic/agent/opentelemetry/translators/redis_datastore_translator.rb +19 -0
  23. data/lib/new_relic/agent/opentelemetry/translators/rpc_translator.rb +63 -0
  24. data/lib/new_relic/agent/opentelemetry_bridge.rb +5 -1
  25. data/lib/new_relic/agent/span_event_aggregator.rb +19 -0
  26. data/lib/new_relic/agent/span_event_primitive.rb +2 -0
  27. data/lib/new_relic/agent/transaction.rb +5 -0
  28. data/lib/new_relic/dependency_detection.rb +2 -1
  29. data/lib/new_relic/version.rb +1 -1
  30. data/newrelic.yml +3 -2
  31. data/newrelic_rpm.gemspec +2 -0
  32. data/test/agent_helper.rb +2 -2
  33. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53ece09719003e8bd215b6b809a5ea7089546577e0107b323b64055601e06171
4
- data.tar.gz: 3e7d00ad842bf03e8612657e8e78669b332be80eaac406713c5ab462eaa4e54e
3
+ metadata.gz: 04f3daab2bb3609b38de8f2a1eb9c16bbbee814b6a7f8536abc4db1f09bc8634
4
+ data.tar.gz: 8c32e76b543d25220865b18f2247489df611bce74c1b755a97fcd79c7ddeb9d3
5
5
  SHA512:
6
- metadata.gz: c12d4ed6d3520e1657917d7ffd21e7db8f4026060cea521e8a33de2f7d98af0bc7e7b2e1049eb74655dea6ab6e558cd16e7410a691f2fe271cff0e9fb6aebf87
7
- data.tar.gz: b67bbe080e6bb3ddf2a3c26f2b2b2ada57780cf9a49fab21c7d8421d3e9748c4250c7e61eae4a1ec1f2a9430df42151ff6cdee97e9e11059578f6c0dabf57b73
6
+ metadata.gz: 369084a1fd6fc7313177f80da8701e51e431062fc5d3d6fc602491a58c4e91afe997262f74ff15a66a451812dbcb40c68634bf580cdbdc0c8053d4bf2c3a12b4
7
+ data.tar.gz: ef014f91db60cd980859de6eda2177dac2f42c49b16d590d69aec2aa4365a160045d2c181badf990944519e824e3fb54f0a52dd5cfef3fe8d836d132d69ce340
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # New Relic Ruby Agent Release Notes
2
2
 
3
+ ## v10.6.0
4
+
5
+ - **Feature: SpanLink events are now supported for the Hybrid Agent**
6
+
7
+ Spans created by an OpenTelemetry API can now have [Span Links](https://opentelemetry.io/docs/concepts/signals/traces/#span-links) associated with them. Links can be added on a span's start, by passing them to the `links` argument, or by calling the `OpenTelemetry::Trace::Span#add_link` API. [PR#3586](https://github.com/newrelic/newrelic-ruby-agent/pull/3586)
8
+
9
+ - **Feature: SpanEvent events are now supported for the Hybrid Agent**
10
+
11
+ Spans created by an OpenTelemetry API can now have [SpanEvent events](https://opentelemetry.io/docs/concepts/signals/traces/#span-events) associated with them via the `OpenTelemetry::Trace::Span#add_event` API. SpanEvent events capture timestamped annotations on a span and are sent to New Relic alongside the parent span. [PR#3587](https://github.com/newrelic/newrelic-ruby-agent/pull/3587)
12
+
13
+ - **Feature: Set Span Kind on all Hybrid Agent spans**
14
+
15
+ Previously, only OpenTelemetry spans translated into external request segments or datastore segments added span kind as an attribute. Now, the agent adds span kind to all OpenTelemetry spans where the value is available. [PR#3589](https://github.com/newrelic/newrelic-ruby-agent/pull/3589)
16
+
17
+ - **Feature: Add support for OpenTelemetry::Tracer#start_root_span**
18
+
19
+ The `OpenTelemetry::Tracer#start_root_span` API can now be used to force a transaction to start for a given span, provided it has a `:server` or `:consumer` span kind. For any other span kinds, it will no-op. This method is most commonly used in background job instrumentation. [PR#3588](https://github.com/newrelic/newrelic-ruby-agent/pull/3588)
20
+
21
+ - **Bugfix: Fix `instrumentation.rails_event_logger: false` not disabling the instrumentation**
22
+
23
+ Setting `instrumentation.rails_event_logger` to `false` was not disabling the Rails.event instrumentation as expected. The instrumentation would still be installed during Rails boot. This has been fixed. [PR#3564](https://github.com/newrelic/newrelic-ruby-agent/pull/3564)
24
+
25
+ - **Bugfix: Normalize boolean-like values to `disabled` for instrumentation config keys**
26
+
27
+ Setting any `instrumentation.*` config key to `false`, `no`, or `off` instead of `disabled` will now resolve to `disabled` and prevent the instrumentation from being installed. [PR#3579](https://github.com/newrelic/newrelic-ruby-agent/pull/3579)
28
+
29
+ - **Bugfix: Per-library logging supportability metrics now reflect each library's instrumentation state**
30
+
31
+ The `Supportability/Logging/Ruby/{library}/{enabled|disabled}` metrics previously echoed `application_logging.enabled` for every library, so disabling a single logging instrumentation or running without one of its gems still reported `enabled`. Each library now reports based on its own instrumentation state. [PR#3571](https://github.com/newrelic/newrelic-ruby-agent/pull/3571)
32
+
3
33
  ## v10.5.0
4
34
 
5
35
  - **Feature: Add Dalli 5.0 support and fix meta protocol instrumentation**
@@ -1792,12 +1792,12 @@ module NewRelic
1792
1792
  :description => 'Controls auto-instrumentation of the LogStasher library at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.'
1793
1793
  },
1794
1794
  :'instrumentation.rails_event_logger' => {
1795
- :default => value_of(:'application_logging.enabled'),
1796
- :documentation_default => 'true',
1795
+ :default => instrumentation_value_from_boolean(:'application_logging.enabled'),
1796
+ :documentation_default => 'enabled',
1797
1797
  :public => true,
1798
- :type => Boolean,
1798
+ :type => String,
1799
1799
  :allowed_from_server => false,
1800
- :description => 'Controls instrumentation of Rails.event as structured logs'
1800
+ :description => 'Controls instrumentation of Rails.event as structured logs. May be one of: `enabled`, `disabled`.'
1801
1801
  },
1802
1802
  :'instrumentation.rails_event_logger.event_names' => {
1803
1803
  :default => [],
@@ -26,6 +26,7 @@ module NewRelic
26
26
  }.freeze
27
27
 
28
28
  INSTRUMENTATION_VALUES = %w[chain prepend unsatisfied]
29
+ INSTRUMENTATION_DISABLED_VALUES = %w[false no off].freeze
29
30
  NUMERIC_TYPES = [Integer, Float]
30
31
  STRINGLIKE_TYPES = [String, Symbol]
31
32
 
@@ -178,6 +179,11 @@ module NewRelic
178
179
  false
179
180
  end
180
181
 
182
+ def instrumentation_key?(key)
183
+ key.to_s.match?(/\Ainstrumentation\.[^.]+\z/) &&
184
+ DEFAULTS.dig(key, :type) == String
185
+ end
186
+
181
187
  def handle_nil_type(key, value, category)
182
188
  return value if %i[manual test].include?(category)
183
189
 
@@ -205,6 +211,7 @@ module NewRelic
205
211
 
206
212
  type = DEFAULTS.dig(key, :type)
207
213
  return handle_nil_type(key, value, category) unless type
214
+ return 'disabled' if instrumentation_key?(key) && INSTRUMENTATION_DISABLED_VALUES.include?(value.to_s.downcase)
208
215
  return value if value.is_a?(type) || boolean?(type, value) || instrumentation?(type, value)
209
216
  return numeric_conversion(value) if NUMERIC_TYPES.include?(type) && NUMERIC_TYPES.include?(value.class)
210
217
  return string_conversion(value) if STRINGLIKE_TYPES.include?(type) && STRINGLIKE_TYPES.include?(value.class)
@@ -80,7 +80,7 @@ module NewRelic
80
80
  unless NewRelic::Agent::Database.record_sql_method == :raw
81
81
  query_plan_string = NewRelic::Agent::Database::PostgresExplainObfuscator.obfuscate(query_plan_string)
82
82
  end
83
- values = query_plan_string.split("\n").map { |line| [line] }
83
+ values = query_plan_string.split("\n").zip
84
84
 
85
85
  [[QUERY_PLAN], values]
86
86
  end
@@ -286,13 +286,13 @@ module NewRelic
286
286
  trace[-keep_frames..-1].unshift("<truncated #{truncate_frames} additional frames>")
287
287
  when 'end'
288
288
  # Remove the end, keep the beginning
289
- trace[0...keep_frames].concat(["<truncated #{truncate_frames} additional frames>"])
289
+ trace[0...keep_frames].push("<truncated #{truncate_frames} additional frames>")
290
290
  else # 'middle' or default (original behavior)
291
291
  # Remove the middle, keep beginning and end
292
292
  # If keep_frames is odd, we will split things up favoring the top of the trace
293
293
  keep_top = (keep_frames / 2.0).ceil
294
294
  keep_bottom = (keep_frames / 2.0).floor
295
- trace[0...keep_top].concat(["<truncated #{truncate_frames} additional frames>"]).concat(trace[-keep_bottom..-1])
295
+ trace[0...keep_top].push("<truncated #{truncate_frames} additional frames>").concat(trace[-keep_bottom..-1])
296
296
  end
297
297
  end
298
298
 
@@ -24,7 +24,13 @@ module NewRelic
24
24
  FORWARDING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Forwarding/Ruby/%s'.freeze
25
25
  DECORATING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/LocalDecorating/Ruby/%s'.freeze
26
26
  LABELS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Labels/Ruby/%s'.freeze
27
- SUPPORTED_LOGGING_LIBRARIES = %w[Logger LogStasher Logging SemanticLogger RailsEventLogger].freeze
27
+ SUPPORTED_LOGGING_LIBRARIES = {
28
+ 'Logger' => :'instrumentation.logger',
29
+ 'LogStasher' => :'instrumentation.logstasher',
30
+ 'Logging' => :'instrumentation.logging',
31
+ 'SemanticLogger' => :'instrumentation.semantic_logger',
32
+ 'RailsEventLogger' => :'instrumentation.rails_event_logger'
33
+ }.freeze
28
34
  MAX_BYTES = 32768 # 32 * 1024 bytes (32 kibibytes)
29
35
  SKIP_SEMANTIC_LOGGER_VARS = %w[@level @level_index @message @time @payload].freeze
30
36
  SKIP_RAILS_EVENT_KEYS = %w[message level].freeze
@@ -479,8 +485,8 @@ module NewRelic
479
485
  record_configuration_metric(FORWARDING_SUPPORTABILITY_FORMAT, FORWARDING_ENABLED_KEY)
480
486
  record_configuration_metric(DECORATING_SUPPORTABILITY_FORMAT, DECORATING_ENABLED_KEY)
481
487
  record_configuration_metric(LABELS_SUPPORTABILITY_FORMAT, LABELS_ENABLED_KEY)
482
- SUPPORTED_LOGGING_LIBRARIES.each do |library|
483
- record_logs_supportability_metric(library, OVERALL_ENABLED_KEY)
488
+ SUPPORTED_LOGGING_LIBRARIES.each do |library, instrumentation_key|
489
+ record_logs_supportability_metric(library, instrumentation_key)
484
490
  end
485
491
 
486
492
  add_custom_attributes(NewRelic::Agent.config[CUSTOM_ATTRIBUTES_KEY])
@@ -492,8 +498,10 @@ module NewRelic
492
498
  NewRelic::Agent.increment_metric(format % label)
493
499
  end
494
500
 
495
- def record_logs_supportability_metric(library, key)
496
- label = supportability_label_for(key)
501
+ def record_logs_supportability_metric(library, instrumentation_key)
502
+ value = NewRelic::Agent.config[instrumentation_key]
503
+ installed = @enabled && value != 'disabled' && value != :unsatisfied
504
+ label = installed ? 'enabled' : 'disabled'
497
505
  NewRelic::Agent.increment_metric("Supportability/Logging/Ruby/#{library}/#{label}")
498
506
  end
499
507
 
@@ -6,6 +6,53 @@ module NewRelic
6
6
  module Agent
7
7
  module OpenTelemetry
8
8
  module AbstractSegmentPatch
9
+ MAX_SPAN_LINKS = 100
10
+ SPAN_LINKS_DROPPED_METRIC = 'Supportability/Ruby/SpanEvent/Links/Dropped'
11
+ MAX_SPAN_EVENTS = 100
12
+ SPAN_EVENTS_DROPPED_METRIC = 'Supportability/Ruby/SpanEvent/Events/Dropped'
13
+
14
+ def add_span_link(link)
15
+ @span_links ||= []
16
+ if @span_links.size >= MAX_SPAN_LINKS
17
+ NewRelic::Agent.record_metric(SPAN_LINKS_DROPPED_METRIC, 1)
18
+ return
19
+ end
20
+ @span_links << link
21
+ end
22
+
23
+ def span_links
24
+ instance_variable_defined?(:@span_links) ? @span_links : NewRelic::EMPTY_ARRAY
25
+ end
26
+
27
+ # This method adds SpanEvent events to a Span event/Segment.
28
+ # A SpanEvent is used to denote a meaningful, singular point in a
29
+ # Span's duration.
30
+ def add_span_event_event(name, attributes: nil, timestamp: nil)
31
+ @span_events ||= []
32
+
33
+ if @span_events.size >= MAX_SPAN_EVENTS
34
+ NewRelic::Agent.record_metric(SPAN_EVENTS_DROPPED_METRIC, 1)
35
+ return
36
+ end
37
+
38
+ # Normalize to Float seconds at storage time.
39
+ # OTel may pass Integer nanoseconds; New Relic uses Float seconds.
40
+ ts = if timestamp.nil?
41
+ Process.clock_gettime(Process::CLOCK_REALTIME)
42
+ elsif timestamp.is_a?(Integer)
43
+ timestamp / 1_000_000_000.0
44
+ else
45
+ timestamp
46
+ end
47
+
48
+ @span_events << {name: name, attributes: attributes, timestamp: ts}
49
+ end
50
+
51
+ # Used to reference SpanEvent events associated with a Span/Segment
52
+ def span_events
53
+ instance_variable_defined?(:@span_events) ? @span_events : NewRelic::EMPTY_ARRAY
54
+ end
55
+
9
56
  def force_finish
10
57
  if instance_variable_defined?(:@otel_span)
11
58
  otel_span = instance_variable_get(:@otel_span)
@@ -3,9 +3,12 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require_relative 'translators/datastore_translator'
6
+ require_relative 'translators/redis_datastore_translator'
6
7
  require_relative 'translators/http_client_translator'
7
8
  require_relative 'translators/http_server_translator'
8
9
  require_relative 'translators/generic_translator'
10
+ require_relative 'translators/rpc_translator'
11
+ require_relative 'translators/messaging_translator'
9
12
 
10
13
  module NewRelic
11
14
  module Agent
@@ -19,19 +22,19 @@ module NewRelic
19
22
  # pg instrumentation doesn't have db.system assigned when connect
20
23
  # spans start, so they would be incorrectly assigned
21
24
  # the HttpClientTranslator
22
- 'opentelemetry-instrumentation-pg' => DatastoreTranslator
23
- # 'opentelemetry-instrumentation-redis' => RedisDatastoreTranslator,
25
+ 'opentelemetry-instrumentation-pg' => DatastoreTranslator,
26
+ 'opentelemetry-instrumentation-redis' => RedisDatastoreTranslator
24
27
  },
25
28
  discriminating_attribute: {
26
29
  'db.system' => DatastoreTranslator,
27
- 'db.system.name' => DatastoreTranslator
28
- # 'rpc.system' => RpcTranslator,
30
+ 'db.system.name' => DatastoreTranslator,
31
+ 'rpc.system' => RpcTranslator
29
32
  },
30
33
  span_kind: {
31
34
  client: HttpClientTranslator,
32
35
  server: HttpServerTranslator,
33
- # consumer: MessagingConsumerTranslator,
34
- # producer: MessagingProducerTranslator,
36
+ consumer: MessagingTranslator,
37
+ producer: MessagingTranslator,
35
38
  internal: GenericTranslator
36
39
  }
37
40
  }.freeze
@@ -64,7 +67,7 @@ module NewRelic
64
67
  GenericTranslator
65
68
  end
66
69
 
67
- translator.translate(attributes: attributes, name: name, instrumentation_scope: instrumentation_scope)
70
+ translator.translate(attributes: attributes, name: name, instrumentation_scope: instrumentation_scope, kind: span_kind)
68
71
  end
69
72
  end
70
73
  end
@@ -0,0 +1,39 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ module NewRelic
6
+ module Agent
7
+ module OpenTelemetry
8
+ module MessagingPatch
9
+ # A producer span with a remote parent MUST start a new
10
+ # transaction. Naming for that case is handled by MessagingPatch
11
+ # since NR's transaction_name only knows about :consume
12
+ PRODUCE = 'Produce/'
13
+
14
+ def transaction_name(library, destination_type, destination_name, action = nil)
15
+ return super unless action == :produce
16
+
17
+ name = Transaction::MESSAGE_PREFIX + library
18
+ name << NewRelic::SLASH
19
+ name << Transaction::MessageBrokerSegment::TYPES[destination_type]
20
+ name << NewRelic::SLASH
21
+ name << PRODUCE
22
+
23
+ case destination_type
24
+ when :queue, :topic, :exchange
25
+ name << Transaction::MessageBrokerSegment::NAMED
26
+ name << destination_name.to_s
27
+ when :temporary_queue, :temporary_topic
28
+ name << Transaction::MessageBrokerSegment::TEMP
29
+ else # handles :stream or :unknown
30
+ name << Transaction::MessageBrokerSegment::NAMED
31
+ name << destination_name.to_s
32
+ end
33
+
34
+ name
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,115 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../attributes'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ module OpenTelemetry
10
+ module SpanEventPrimitivePatch
11
+ SPAN_LINK_TYPE = 'SpanLink'
12
+ SPAN_EVENT_TYPE = 'SpanEvent'
13
+ ID_KEY = 'id'
14
+ LINKED_SPAN_ID_KEY = 'linkedSpanId'
15
+ LINKED_TRACE_ID_KEY = 'linkedTraceId'
16
+ TRACE_DOT_ID_KEY = 'trace.id'
17
+ SPAN_DOT_ID_KEY = 'span.id'
18
+
19
+ def for_segment(segment)
20
+ span_data = super
21
+ update_spans_for_otel(span_data, segment)
22
+ end
23
+
24
+ def for_external_request_segment(segment)
25
+ span_data = super
26
+ update_spans_for_otel(span_data, segment)
27
+ end
28
+
29
+ def for_datastore_segment(segment)
30
+ span_data = super
31
+ update_spans_for_otel(span_data, segment)
32
+ end
33
+
34
+ private
35
+
36
+ def update_spans_for_otel(span_data, segment)
37
+ span_data = attach_span_kind(span_data, segment)
38
+ span_data = attach_span_links(span_data, segment)
39
+ attach_span_event_events(span_data, segment)
40
+ end
41
+
42
+ def attach_span_kind(span_data, segment)
43
+ if segment.instance_variable_defined?(:@otel_span)
44
+ otel_span = segment.instance_variable_get(:@otel_span)
45
+ kind = otel_span.kind&.to_s
46
+ span_data[0][SpanEventPrimitive::SPAN_KIND_KEY] = kind if kind
47
+
48
+ span_data
49
+ end
50
+ end
51
+
52
+ def attach_span_links(span_data, segment)
53
+ links = for_span_links(segment)
54
+ links.empty? ? span_data : span_data << links
55
+ end
56
+
57
+ def for_span_links(segment)
58
+ return [] if segment.span_links.empty?
59
+
60
+ segment.span_links.map do |link|
61
+ ctx = link.span_context
62
+ intrinsics = {
63
+ SpanEventPrimitive::TYPE_KEY => SPAN_LINK_TYPE,
64
+ SpanEventPrimitive::TIMESTAMP_KEY => milliseconds_since_epoch(segment),
65
+ ID_KEY => segment.guid,
66
+ TRACE_DOT_ID_KEY => segment.transaction.trace_id,
67
+ LINKED_SPAN_ID_KEY => ctx.hex_span_id,
68
+ LINKED_TRACE_ID_KEY => ctx.hex_trace_id
69
+ }
70
+ [intrinsics, sanitize_event_attributes(link.attributes), {}]
71
+ end
72
+ end
73
+
74
+ def attach_span_event_events(span_data, segment)
75
+ span_events = for_span_event_events(segment)
76
+ return span_data if span_events.empty?
77
+
78
+ if span_data.length > 3
79
+ span_data[3].concat(span_events)
80
+ else
81
+ span_data << span_events
82
+ end
83
+
84
+ span_data
85
+ end
86
+
87
+ def for_span_event_events(segment)
88
+ return [] if segment.span_events.empty?
89
+
90
+ segment.span_events.map do |evt|
91
+ intrinsics = {
92
+ SpanEventPrimitive::TYPE_KEY => SPAN_EVENT_TYPE,
93
+ # This is the same formula as milliseconds_since_epoch, but without
94
+ # the segment.start_time value
95
+ SpanEventPrimitive::TIMESTAMP_KEY => Integer(evt[:timestamp].to_f * 1000.0),
96
+ SPAN_DOT_ID_KEY => segment.guid,
97
+ TRACE_DOT_ID_KEY => segment.transaction.trace_id,
98
+ SpanEventPrimitive::NAME_KEY => evt[:name]
99
+ }
100
+ [intrinsics, sanitize_event_attributes(evt[:attributes]), {}]
101
+ end
102
+ end
103
+
104
+ def sanitize_event_attributes(attrs)
105
+ return NewRelic::EMPTY_HASH if attrs.nil? || attrs.empty?
106
+
107
+ filter = NewRelic::Agent.instance.attribute_filter
108
+ attr_obj = Attributes.new(filter)
109
+ attr_obj.merge_custom_attributes(attrs)
110
+ attr_obj.custom_attributes_for(AttributeFilter::DST_SPAN_EVENTS)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -8,6 +8,7 @@ module NewRelic
8
8
  module Trace
9
9
  class Span < ::OpenTelemetry::Trace::Span
10
10
  attr_accessor :finishable
11
+ attr_accessor :kind
11
12
  attr_writer :translator
12
13
  attr_reader :status
13
14
 
@@ -23,7 +24,7 @@ module NewRelic
23
24
  return if attributes.nil? || attributes.empty?
24
25
 
25
26
  if @translator
26
- translated = @translator.translate(attributes: attributes)
27
+ translated = @translator.translate(attributes: attributes, kind: @kind)
27
28
  apply_translated_attributes(translated)
28
29
  else
29
30
  # fallback to adding all attributes to the span as custom
@@ -31,6 +32,27 @@ module NewRelic
31
32
  end
32
33
  end
33
34
 
35
+ def add_link(link)
36
+ return self unless recording?
37
+
38
+ # add_span_link lives in OpenTelemetry::AbstractSegmentPatch
39
+ # and in OpenTelemetry::TransactionPatch
40
+ finishable&.add_span_link(link)
41
+ self
42
+ end
43
+
44
+ def add_event(name, attributes: nil, timestamp: nil)
45
+ # This method adds SpanEvent events to a Span event.
46
+ # A SpanEvent is used to denote a meaningful, singular point in a
47
+ # Span's duration.
48
+ return self unless recording?
49
+
50
+ # add_span_event_event lives in OpenTelemetry::AbstractSegmentPatch
51
+ # and in OpenTelemetry::TransactionPatch
52
+ finishable&.add_span_event_event(name, attributes: attributes, timestamp: timestamp)
53
+ self
54
+ end
55
+
34
56
  def apply_translated_attributes(translated)
35
57
  translated[:instance_variable].each do |key, value|
36
58
  sym_key = "@#{key}".to_sym
@@ -35,10 +35,12 @@ module NewRelic
35
35
 
36
36
  otel_span.finishable = finishable
37
37
  otel_span.status = ::OpenTelemetry::Trace::Status.unset
38
+ otel_span.kind = kind
38
39
  otel_span.translator = translated[:translator]
39
40
  add_remote_context_to_otel_span(otel_span, parent_otel_context)
40
41
  otel_span.add_instrumentation_scope(@name, @version)
41
42
  otel_span.apply_translated_attributes(translated)
43
+ links&.each { |link| otel_span.add_link(link) }
42
44
 
43
45
  otel_span
44
46
  end
@@ -56,6 +58,10 @@ module NewRelic
56
58
  span&.finish
57
59
  end
58
60
 
61
+ def start_root_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil)
62
+ start_span(name, with_parent: ::OpenTelemetry::Context.empty, attributes: attributes, links: links, start_timestamp: start_timestamp, kind: kind)
63
+ end
64
+
59
65
  private
60
66
 
61
67
  def start_segment_from_otel(name:, translated: nil, start_timestamp: nil, kind: nil)
@@ -63,9 +69,9 @@ module NewRelic
63
69
 
64
70
  case kind
65
71
  when :client
66
- if segment_api_params[:uri] # HTTP Client
72
+ if segment_api_params[:uri] # HTTP Client and gRPC Client
67
73
  NewRelic::Agent::Tracer.start_external_request_segment(
68
- library: @name,
74
+ library: segment_api_params[:library] || @name,
69
75
  uri: segment_api_params[:uri],
70
76
  procedure: segment_api_params[:procedure],
71
77
  start_time: start_timestamp
@@ -81,12 +87,29 @@ module NewRelic
81
87
  start_time: start_timestamp
82
88
  )
83
89
 
84
- segment&._notice_sql(segment_api_params[:sql])
90
+ if segment_api_params[:sql]
91
+ segment&._notice_sql(segment_api_params[:sql])
92
+ elsif segment_api_params[:nosql_statement]
93
+ segment&.notice_nosql_statement(segment_api_params[:nosql_statement])
94
+ end
85
95
 
86
96
  segment
87
97
  else
88
98
  NewRelic::Agent::Tracer.start_segment(name: name)
89
99
  end
100
+ when :producer, :consumer
101
+ if segment_api_params[:library] # Messaging
102
+ NewRelic::Agent::Tracer.start_message_broker_segment(
103
+ action: segment_api_params[:action],
104
+ library: segment_api_params[:library],
105
+ destination_type: segment_api_params[:destination_type],
106
+ destination_name: segment_api_params[:destination_name],
107
+ parameters: messaging_segment_parameters(segment_api_params),
108
+ start_time: start_timestamp
109
+ )
110
+ else
111
+ NewRelic::Agent::Tracer.start_segment(name: name)
112
+ end
90
113
  else
91
114
  NewRelic::Agent::Tracer.start_segment(name: name)
92
115
  end
@@ -97,14 +120,16 @@ module NewRelic
97
120
  segment_api_params = translated[:for_segment_api]
98
121
 
99
122
  case kind
100
- when :server
123
+ when :server # HTTP or gRPC server calls
101
124
  nr_item = NewRelic::Agent::Tracer.start_transaction_or_segment(
102
125
  name: segment_api_params[:name] || name,
103
126
  category: :web
104
127
  )
105
128
  when :client
106
129
  nr_item = NewRelic::Agent::Tracer.start_transaction_or_segment(name: name, category: :web)
107
- when :consumer, :producer, :internal, nil
130
+ when :consumer, :producer
131
+ nr_item = start_messaging_transaction(name, segment_api_params)
132
+ when :internal, nil
108
133
  nr_item = NewRelic::Agent::Tracer.start_transaction_or_segment(name: name, category: :task)
109
134
  end
110
135
 
@@ -113,6 +138,21 @@ module NewRelic
113
138
  nr_item
114
139
  end
115
140
 
141
+ def messaging_segment_parameters(segment_api_params)
142
+ return nil unless NewRelic::Agent.config[:'message_tracer.segment_parameters.enabled']
143
+
144
+ segment_api_params[:parameters]
145
+ end
146
+
147
+ def start_messaging_transaction(name, segment_api_params)
148
+ return NewRelic::Agent::Tracer.start_transaction_or_segment(name: name, category: :task) unless segment_api_params[:name]
149
+
150
+ NewRelic::Agent::Tracer.start_transaction_or_segment(
151
+ name: segment_api_params[:name],
152
+ category: :message
153
+ )
154
+ end
155
+
116
156
  def get_otel_span_from_finishable(finishable)
117
157
  case finishable
118
158
  when NewRelic::Agent::Transaction
@@ -31,6 +31,26 @@ module NewRelic
31
31
  super
32
32
  end
33
33
 
34
+ def add_span_link(link)
35
+ initial_segment&.add_span_link(link)
36
+ end
37
+
38
+ def span_links
39
+ initial_segment&.span_links || NewRelic::EMPTY_ARRAY
40
+ end
41
+
42
+ # This method adds a SpanEvent event to the Transaction's initial segment.
43
+ # A SpanEvent is used to denote a meaningful, singular point in a
44
+ # Span's duration.
45
+ def add_span_event_event(name, attributes: nil, timestamp: nil)
46
+ initial_segment&.add_span_event_event(name, attributes: attributes, timestamp: timestamp)
47
+ end
48
+
49
+ # Used to reference SpanEvent events associated with the initial segment.
50
+ def span_events
51
+ initial_segment&.span_events || NewRelic::EMPTY_ARRAY
52
+ end
53
+
34
54
  private
35
55
 
36
56
  def find_or_create_span(segment)
@@ -97,6 +97,33 @@ module NewRelic
97
97
  }
98
98
  }.freeze
99
99
 
100
+ REDIS_MAPPINGS = { # v1.25, v1.17
101
+ 'product' => {
102
+ otel_keys: ['db.system.name', 'db.system'],
103
+ segment_field: :product
104
+ },
105
+ 'database_name' => {
106
+ otel_keys: ['db.namespace', 'db.name'],
107
+ segment_field: :database_name
108
+ },
109
+ 'host' => {
110
+ otel_keys: ['server.address', 'net.peer.name'],
111
+ segment_field: :host
112
+ },
113
+ 'port_path_or_id' => {
114
+ otel_keys: ['server.port', 'net.peer.port'],
115
+ segment_field: :port_path_or_id
116
+ },
117
+ 'operation' => {
118
+ otel_keys: ['db.operation.name', 'db.operation'],
119
+ segment_field: :operation
120
+ },
121
+ 'nosql_statement' => {
122
+ otel_keys: ['db.statement', 'db.query.text'],
123
+ segment_field: :nosql_statement
124
+ }
125
+ }.freeze
126
+
100
127
  HTTP_SERVER_MAPPINGS = { # v1.23, v1.20
101
128
  'http_response_code' => {
102
129
  otel_keys: ['http.response.status_code', 'http.status_code'],
@@ -123,6 +150,106 @@ module NewRelic
123
150
  destinations: DEFAULT_DESTINATIONS
124
151
  }
125
152
  }.freeze
153
+
154
+ RPC_SERVER_MAPPINGS = { # v1.20
155
+ 'response_status' => {
156
+ otel_keys: ['rpc.grpc.status_code'],
157
+ category: :instance_variable
158
+ },
159
+ 'request.headers.host' => {
160
+ otel_keys: ['net.sock.peer.addr', 'server.address', 'net.peer.name'],
161
+ category: :agent,
162
+ destinations: DEFAULT_DESTINATIONS
163
+ },
164
+ 'request.method' => {
165
+ otel_keys: ['rpc.method'],
166
+ category: :agent,
167
+ destinations: DEFAULT_DESTINATIONS
168
+ }
169
+ }.freeze
170
+
171
+ RPC_CLIENT_MAPPINGS = { # v1.17
172
+ 'grpc_status_code' => {
173
+ otel_keys: ['rpc.grpc.status_code'],
174
+ category: :instance_variable
175
+ },
176
+ # the value closest to library in intention is rpc.system. however,
177
+ # to get the correct segment name, we need to set this to rpc.service
178
+ 'library' => {
179
+ otel_keys: ['rpc.service'],
180
+ segment_field: :library
181
+ },
182
+ 'procedure' => {
183
+ otel_keys: ['rpc.method'],
184
+ segment_field: :procedure
185
+ },
186
+ 'host' => {
187
+ # traditional host keys aren't sent by otel grpc instrumentation
188
+ # so we use net.sock.peer.address to fill the host field
189
+ otel_keys: ['net.sock.peer.addr', 'server.address', 'net.peer.name'],
190
+ segment_field: :host
191
+ },
192
+ # not sent by gRPC OTel instrumentation, but can sometimes be gleaned
193
+ # by net.sock.peer.addr
194
+ 'port' => {
195
+ otel_keys: ['server.port', 'net.peer.port'],
196
+ segment_field: :port
197
+ }
198
+ }.freeze
199
+
200
+ MESSAGING_CONSUMER_MAPPINGS = { # v1.30/v1.24, v1.17
201
+ 'messaging.system' => {
202
+ otel_keys: ['messaging.system'],
203
+ category: :agent,
204
+ destinations: AttributeFilter::DST_TRANSACTION_TRACER
205
+ },
206
+ 'message.queueName' => {
207
+ otel_keys: ['messaging.destination.name', 'messaging.destination'],
208
+ category: :agent,
209
+ destinations: DEFAULT_DESTINATIONS,
210
+ segment_field: :destination_name
211
+ },
212
+ 'port' => {
213
+ otel_keys: ['server.port', 'net.peer.port'],
214
+ category: :agent,
215
+ destinations: DEFAULT_DESTINATIONS
216
+ },
217
+ 'host' => {
218
+ otel_keys: ['server.address', 'net.peer.name'],
219
+ category: :agent,
220
+ destinations: DEFAULT_DESTINATIONS
221
+ },
222
+ 'message.routingKey' => {
223
+ otel_keys: [
224
+ 'messaging.kafka.message.key',
225
+ 'messaging.rabbitmq.destination.routing_key'
226
+ ],
227
+ category: :agent,
228
+ destinations: DEFAULT_DESTINATIONS
229
+ }
230
+ }.freeze
231
+
232
+ MESSAGING_PRODUCER_MAPPINGS = { # v1.30/v1.24, v1.17
233
+ 'messaging.system' => {
234
+ otel_keys: ['messaging.system'],
235
+ category: :agent,
236
+ destinations: AttributeFilter::DST_TRANSACTION_TRACER
237
+ },
238
+ 'destination_name' => {
239
+ otel_keys: ['messaging.destination.name', 'messaging.destination'],
240
+ segment_field: :destination_name
241
+ },
242
+ 'port' => {
243
+ otel_keys: ['server.port', 'net.peer.port'],
244
+ category: :agent,
245
+ destinations: DEFAULT_DESTINATIONS
246
+ },
247
+ 'host' => {
248
+ otel_keys: ['server.address', 'net.peer.name'],
249
+ category: :agent,
250
+ destinations: DEFAULT_DESTINATIONS
251
+ }
252
+ }.freeze
126
253
  end
127
254
  end
128
255
  end
@@ -14,7 +14,7 @@ module NewRelic
14
14
  # This method should be redefined in child classes.
15
15
  # The body of the method should be the mappings constant defined in the
16
16
  # AttributesMappings module for the attribute type being translated
17
- def mappings_hash
17
+ def mappings_hash(kind)
18
18
  # no-op
19
19
  NewRelic::EMPTY_HASH
20
20
  end
@@ -28,11 +28,11 @@ module NewRelic
28
28
  #
29
29
  # @return [Hash] A hash with attributes divided into various categories for assignment on a New Relic
30
30
  # transaction or segment.
31
- def translate(attributes: {}, name: nil, instrumentation_scope: nil)
31
+ def translate(attributes: {}, name: nil, instrumentation_scope: nil, kind: nil)
32
32
  working_attrs = attributes.dup # shallow copy
33
33
  result = {intrinsic: {}, agent: {}, custom: {}, for_segment_api: {}, instance_variable: {}, translator: self}
34
34
 
35
- mappings_hash.each do |nr_key, mapping|
35
+ mappings_hash(kind).each do |nr_key, mapping|
36
36
  value = extract_first_present(working_attrs, mapping[:otel_keys])
37
37
  next unless value
38
38
 
@@ -54,7 +54,7 @@ module NewRelic
54
54
  # specialized attributes.
55
55
  # This method will augment the working result hash, so it does not
56
56
  # need to be merged.
57
- add_specialized_attributes(result: result, name: name, attributes: attributes, instrumentation_scope: instrumentation_scope)
57
+ add_specialized_attributes(result: result, name: name, attributes: attributes, instrumentation_scope: instrumentation_scope, kind: kind)
58
58
 
59
59
  # Assign any remaining attributes as custom attributes
60
60
  result[:custom] = working_attrs
@@ -71,7 +71,7 @@ module NewRelic
71
71
  # @param [optional, String] instrumentation_scope The instrumentation scope provided to the translate method
72
72
  #
73
73
  # @return [Hash] The augmented result hash
74
- def add_specialized_attributes(result: {}, name: nil, attributes: {}, instrumentation_scope: nil)
74
+ def add_specialized_attributes(result: {}, name: nil, attributes: {}, instrumentation_scope: nil, kind: nil)
75
75
  # no-op
76
76
  {}
77
77
  end
@@ -9,11 +9,11 @@ module NewRelic
9
9
  module OpenTelemetry
10
10
  class DatastoreTranslator < BaseTranslator
11
11
  class << self
12
- def mappings_hash
12
+ def mappings_hash(_kind)
13
13
  AttributeMappings::DATASTORE_MAPPINGS
14
14
  end
15
15
 
16
- def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil)
16
+ def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil, kind: nil)
17
17
  operation = parse_operation(name, attributes)
18
18
  result[:for_segment_api][:operation] = operation if operation
19
19
 
@@ -11,7 +11,7 @@ module NewRelic
11
11
  # because there are no known New Relic attributes to translate
12
12
  class GenericTranslator < BaseTranslator
13
13
  class << self
14
- def mappings_hash
14
+ def mappings_hash(_kind)
15
15
  NewRelic::EMPTY_HASH
16
16
  end
17
17
  end
@@ -9,11 +9,11 @@ module NewRelic
9
9
  module OpenTelemetry
10
10
  class HttpClientTranslator < BaseTranslator
11
11
  class << self
12
- def mappings_hash
12
+ def mappings_hash(_kind)
13
13
  AttributeMappings::HTTP_CLIENT_MAPPINGS
14
14
  end
15
15
 
16
- def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil)
16
+ def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil, kind: nil)
17
17
  uri = build_uri(attributes)
18
18
  result[:for_segment_api][:uri] = uri if uri
19
19
 
@@ -9,11 +9,11 @@ module NewRelic
9
9
  module OpenTelemetry
10
10
  class HttpServerTranslator < BaseTranslator
11
11
  class << self
12
- def mappings_hash
12
+ def mappings_hash(_kind)
13
13
  AttributeMappings::HTTP_SERVER_MAPPINGS
14
14
  end
15
15
 
16
- def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil)
16
+ def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil, kind: nil)
17
17
  result[:for_segment_api][:name] = create_server_transaction_name(name, instrumentation_scope, attributes)
18
18
 
19
19
  result
@@ -0,0 +1,128 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'base_translator'
6
+ require_relative '../../messaging'
7
+
8
+ module NewRelic
9
+ module Agent
10
+ module OpenTelemetry
11
+ class MessagingTranslator < BaseTranslator
12
+ # OTel seems to be inconsistent with dot or underscore,
13
+ # so we handle both just in case.
14
+ DESTINATION_TYPES_MAP = {
15
+ consumer: {
16
+ 'kafka' => :topic,
17
+ 'rabbitmq' => :queue,
18
+ 'aws.sqs' => :queue,
19
+ 'aws_sqs' => :queue,
20
+ 'aws.sns' => :topic,
21
+ 'aws_sns' => :topic,
22
+ 'aws.kinesis' => :stream,
23
+ 'aws_kinesis' => :stream
24
+ },
25
+ producer: {
26
+ 'kafka' => :topic,
27
+ 'rabbitmq' => :exchange,
28
+ 'aws.sqs' => :queue,
29
+ 'aws_sqs' => :queue,
30
+ 'aws.sns' => :topic,
31
+ 'aws_sns' => :topic,
32
+ 'aws.kinesis' => :stream,
33
+ 'aws_kinesis' => :stream
34
+ }
35
+ }.freeze
36
+
37
+ # `messaging.destination_kind` is a v1.17 attribute that we
38
+ # should honor when present.
39
+ DESTINATION_KIND_MAP = {
40
+ 'queue' => :queue,
41
+ 'topic' => :topic
42
+ }.freeze
43
+
44
+ TEMP_MAP = {
45
+ queue: :temporary_queue,
46
+ topic: :temporary_topic
47
+ }.freeze
48
+
49
+ ACTION_MAP = {
50
+ consumer: :consume,
51
+ producer: :produce
52
+ }.freeze
53
+
54
+ # Capitalize the OTel `messaging.system` value to match the casing used
55
+ # by NR messaging instrumentation.
56
+ LIBRARY_MAP = {
57
+ 'kafka' => 'Kafka',
58
+ 'rabbitmq' => 'RabbitMQ',
59
+ 'aws.sqs' => 'SQS',
60
+ 'aws_sqs' => 'SQS',
61
+ 'aws.sns' => 'SNS',
62
+ 'aws_sns' => 'SNS',
63
+ 'aws.kinesis' => 'Kinesis',
64
+ 'aws_kinesis' => 'Kinesis'
65
+ }.freeze
66
+
67
+ ROUTING_KEY_OTEL_KEYS = [
68
+ 'messaging.kafka.message.key',
69
+ 'messaging.rabbitmq.destination.routing_key'
70
+ ].freeze
71
+ CORRELATION_ID_OTEL_KEY = 'messaging.message.conversation_id'
72
+
73
+ class << self
74
+ def mappings_hash(kind)
75
+ kind == :consumer ? AttributeMappings::MESSAGING_CONSUMER_MAPPINGS : AttributeMappings::MESSAGING_PRODUCER_MAPPINGS
76
+ end
77
+
78
+ def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil, kind: nil)
79
+ result[:for_segment_api][:action] = ACTION_MAP[kind]
80
+ result[:for_segment_api][:destination_type] = determine_destination_type(attributes, kind)
81
+ if (system = attributes['messaging.system'])
82
+ result[:for_segment_api][:library] = LIBRARY_MAP[system] || system
83
+ end
84
+
85
+ result[:for_segment_api][:name] = build_transaction_name(result[:for_segment_api])
86
+
87
+ add_producer_segment_params(result, attributes) if kind == :producer
88
+
89
+ result
90
+ end
91
+
92
+ def build_transaction_name(segment_api_params)
93
+ return nil unless segment_api_params[:library] && segment_api_params[:destination_name]
94
+
95
+ NewRelic::Agent::Messaging.transaction_name(
96
+ segment_api_params[:library],
97
+ segment_api_params[:destination_type],
98
+ segment_api_params[:destination_name],
99
+ segment_api_params[:action]
100
+ )
101
+ end
102
+
103
+ def add_producer_segment_params(result, attributes)
104
+ params = {
105
+ routing_key: ROUTING_KEY_OTEL_KEYS.map { |k| attributes[k] }.compact.first,
106
+ correlation_id: attributes[CORRELATION_ID_OTEL_KEY]
107
+ }.compact
108
+
109
+ result[:for_segment_api][:parameters] = params unless params.empty?
110
+ end
111
+
112
+ def determine_destination_type(attributes, kind)
113
+ type = explicit_destination_kind(attributes) ||
114
+ DESTINATION_TYPES_MAP[kind][attributes['messaging.system']] ||
115
+ :unknown
116
+ return type unless attributes['messaging.destination.temporary'] == true
117
+
118
+ TEMP_MAP[type] || type
119
+ end
120
+
121
+ def explicit_destination_kind(attributes)
122
+ DESTINATION_KIND_MAP[attributes['messaging.destination_kind']]
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,19 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'datastore_translator'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ module OpenTelemetry
10
+ class RedisDatastoreTranslator < DatastoreTranslator
11
+ class << self
12
+ def mappings_hash(_kind)
13
+ AttributeMappings::REDIS_MAPPINGS
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'base_translator'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ module OpenTelemetry
10
+ class RpcTranslator < BaseTranslator
11
+ class << self
12
+ def mappings_hash(kind)
13
+ kind == :client ? AttributeMappings::RPC_CLIENT_MAPPINGS : AttributeMappings::RPC_SERVER_MAPPINGS
14
+ end
15
+
16
+ def add_specialized_attributes(result: {}, name: nil, attributes: nil, instrumentation_scope: nil, kind: nil)
17
+ case kind
18
+ when :client
19
+ uri = build_uri(attributes)
20
+ result[:for_segment_api][:uri] = uri if uri
21
+ when :server
22
+ server_name = create_server_transaction_name(name: name, attributes: attributes, instrumentation_scope: instrumentation_scope) if name
23
+ result[:for_segment_api][:name] = server_name if server_name
24
+
25
+ uri = build_uri(attributes)
26
+ if uri
27
+ result[:agent]['request.uri'] = {value: uri, destinations: AttributeMappings::DEFAULT_DESTINATIONS}
28
+ end
29
+ end
30
+
31
+ result
32
+ end
33
+
34
+ def create_server_transaction_name(name: nil, attributes: nil, instrumentation_scope: nil)
35
+ txn_name = name&.delete_prefix(NewRelic::SLASH)
36
+ Instrumentation::ControllerInstrumentation::TransactionNamer.name_for(nil, nil, :web, {class_name: instrumentation_scope, name: txn_name})
37
+ end
38
+
39
+ def build_uri(attributes)
40
+ host = attributes['server.address'] || attributes['net.peer.name'] || attributes['net.sock.peer.addr']
41
+ service = attributes['rpc.service']
42
+ method = attributes['rpc.method']
43
+
44
+ # return early if this method is called with an attributes
45
+ # hash that doesn't have the required values
46
+ return unless host || service || method
47
+
48
+ # strip scheme prefix (e.g. dns:///, xds:///) used by gRPC name resolvers
49
+ host = host&.sub(%r{\A\w+:///}, '')
50
+
51
+ if host && service && method
52
+ "grpc://#{host}/#{service}/#{method}"
53
+ elsif host && method
54
+ "grpc://#{host}/#{method}"
55
+ elsif service && method
56
+ "grpc://#{service}/#{method}"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -70,12 +70,16 @@ module NewRelic
70
70
  require_relative 'opentelemetry/context'
71
71
  require_relative 'opentelemetry/trace_patch'
72
72
  require_relative 'opentelemetry/abstract_segment_patch'
73
+ require_relative 'opentelemetry/messaging_patch'
74
+ require_relative 'opentelemetry/span_event_primitive_patch'
73
75
 
74
76
  NewRelic::Agent.logger.warn('OpenTelemetry SDK gem is installed. This may interfere with New Relic instrumentation.') if defined?(OpenTelemetry::SDK)
75
77
 
76
- ::OpenTelemetry::Trace.singleton_class.prepend(NewRelic::Agent::OpenTelemetry::TracePatch)
78
+ ::OpenTelemetry::Trace.singleton_class.prepend(OpenTelemetry::TracePatch)
77
79
  Transaction.prepend(OpenTelemetry::TransactionPatch)
78
80
  Transaction::AbstractSegment.prepend(OpenTelemetry::AbstractSegmentPatch)
81
+ Messaging.singleton_class.prepend(OpenTelemetry::MessagingPatch)
82
+ SpanEventPrimitive.prepend(OpenTelemetry::SpanEventPrimitivePatch)
79
83
 
80
84
  ::OpenTelemetry.tracer_provider = OpenTelemetry::Trace::TracerProvider.new
81
85
  ::OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::TracePropagator.new
@@ -33,6 +33,25 @@ module NewRelic
33
33
  SUPPORTABILITY_TOTAL_SENT = 'Supportability/SpanEvent/TotalEventsSent'.freeze
34
34
  SUPPORTABILITY_DISCARDED = 'Supportability/SpanEvent/Discarded'.freeze
35
35
 
36
+ def harvest!
37
+ metadata, events = super
38
+ # extra_events is for SpanLink and SpanEvent data related to a Span
39
+ # SpanLink and SpanEvent events are both created by OpenTelemetry APIs
40
+ extra_events = []
41
+ events.each do |event|
42
+ # Spans are normally: [intrinsics, custom_attrs, agent_attrs].
43
+ # When a span has links, a 4th element is appended:
44
+ # [intrinsics, custom_attrs, agent_attrs, extra_events]
45
+ #
46
+ # SpanLinks and SpanEvents must be sent to the backend at the top
47
+ # level of the main payload, not nested inside an individual Span.
48
+ # This pulls them out to be flattened into the final array:
49
+ # [Span, SpanLink, Span, SpanEvent, Span]
50
+ extra_events.concat(event.slice!(3)) if event.length > 3
51
+ end
52
+ [metadata, events.concat(extra_events)]
53
+ end
54
+
36
55
  def after_harvest(metadata)
37
56
  seen = metadata[:seen]
38
57
  sent = metadata[:captured]
@@ -27,6 +27,7 @@ module NewRelic
27
27
  DURATION_KEY = 'duration'
28
28
  NAME_KEY = 'name'
29
29
  CATEGORY_KEY = 'category'
30
+ GRPC_STATUS_CODE_KEY = 'grpc.statusCode'
30
31
  HTTP_URL_KEY = 'http.url'
31
32
  HTTP_METHOD_KEY = 'http.method'
32
33
  HTTP_REQUEST_METHOD_KEY = 'http.request.method'
@@ -79,6 +80,7 @@ module NewRelic
79
80
  intrinsics[HTTP_METHOD_KEY] = segment.procedure
80
81
  intrinsics[HTTP_REQUEST_METHOD_KEY] = segment.procedure
81
82
  intrinsics[HTTP_STATUS_CODE_KEY] = segment.http_status_code if segment.http_status_code
83
+ intrinsics[GRPC_STATUS_CODE_KEY] = segment.instance_variable_get(:@grpc_status_code) if segment.instance_variable_defined?(:@grpc_status_code)
82
84
  intrinsics[CATEGORY_KEY] = HTTP_CATEGORY
83
85
  intrinsics[SPAN_KIND_KEY] = CLIENT
84
86
  intrinsics[SERVER_ADDRESS_KEY] = segment.uri.host
@@ -68,6 +68,7 @@ module NewRelic
68
68
  :jruby_cpu_start,
69
69
  :process_cpu_start,
70
70
  :http_response_code,
71
+ :response_status,
71
72
  :response_content_length,
72
73
  :response_content_type,
73
74
  :parent_span_id
@@ -633,6 +634,10 @@ module NewRelic
633
634
  add_agent_attribute(:'http.statusCode', http_response_code, default_destinations)
634
635
  end
635
636
 
637
+ if response_status
638
+ add_agent_attribute(:'response.status', response_status, default_destinations)
639
+ end
640
+
636
641
  if response_content_length
637
642
  add_agent_attribute(:'response.headers.contentLength', response_content_length.to_i, default_destinations)
638
643
  end
@@ -70,7 +70,8 @@ module DependencyDetection
70
70
  # https://github.com/newrelic/newrelic-ruby-agent/issues/2912
71
71
  return if name == :padrino
72
72
 
73
- NewRelic::Agent.config.instance_variable_get(:@cache)[config_key] = :unsatisfied
73
+ cache = NewRelic::Agent.config.instance_variable_get(:@cache)
74
+ cache[config_key] = config_key && cache[config_key] == false ? :disabled : :unsatisfied
74
75
  end
75
76
 
76
77
  def source_location_for(klass, method_name)
@@ -6,7 +6,7 @@
6
6
  module NewRelic
7
7
  module VERSION # :nodoc:
8
8
  MAJOR = 10
9
- MINOR = 5
9
+ MINOR = 6
10
10
  TINY = 0
11
11
 
12
12
  STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
data/newrelic.yml CHANGED
@@ -657,8 +657,9 @@ common: &default_settings
657
657
  # auto, prepend, chain, disabled.
658
658
  # instrumentation.rack_urlmap: auto
659
659
 
660
- # Controls instrumentation of Rails.event as structured logs
661
- # instrumentation.rails_event_logger: true
660
+ # Controls instrumentation of Rails.event as structured logs. May be one of:
661
+ # enabled, disabled.
662
+ # instrumentation.rails_event_logger: enabled
662
663
 
663
664
  # An array of Rails.event names to capture as structured log events. For
664
665
  # example,
data/newrelic_rpm.gemspec CHANGED
@@ -54,6 +54,8 @@ Gem::Specification.new do |s|
54
54
  s.add_development_dependency 'bundler'
55
55
  s.add_development_dependency 'feedjira', '3.2.1' unless ENV['CI']
56
56
  s.add_development_dependency 'httparty' unless ENV['CI'] # for perf tests and Gabby
57
+ # TODO: MAJOR VERSION - Remove support for Minitest 4.7.5 and all calls to
58
+ # skip_unless_minitest5_or_above when we drop support for Ruby 2.7
57
59
  s.add_development_dependency 'minitest', "#{RUBY_VERSION >= '2.7.0' ? '5.3.3' : '4.7.5'}"
58
60
  s.add_development_dependency 'minitest-stub-const', '0.6'
59
61
  s.add_development_dependency 'mocha', '~> 1.16'
data/test/agent_helper.rb CHANGED
@@ -30,8 +30,8 @@ def fake_guid(length = 16)
30
30
  NewRelic::Agent::GuidGenerator.generate_guid(length)
31
31
  end
32
32
 
33
- def assert_between(floor, ceiling, value, message = "expected #{floor} <= #{value} <= #{ceiling}")
34
- assert((floor <= value && value <= ceiling), message)
33
+ def assert_between(floor, ceiling, value, message = "expected #{value} to be between #{floor} and #{ceiling}")
34
+ assert((value.between?(floor, ceiling)), message)
35
35
  end
36
36
 
37
37
  def assert_in_delta(expected, actual, delta)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: newrelic_rpm
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.5.0
4
+ version: 10.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tanna McClure
@@ -639,6 +639,8 @@ files:
639
639
  - lib/new_relic/agent/opentelemetry/context.rb
640
640
  - lib/new_relic/agent/opentelemetry/context/propagation.rb
641
641
  - lib/new_relic/agent/opentelemetry/context/propagation/trace_propagator.rb
642
+ - lib/new_relic/agent/opentelemetry/messaging_patch.rb
643
+ - lib/new_relic/agent/opentelemetry/span_event_primitive_patch.rb
642
644
  - lib/new_relic/agent/opentelemetry/trace.rb
643
645
  - lib/new_relic/agent/opentelemetry/trace/span.rb
644
646
  - lib/new_relic/agent/opentelemetry/trace/tracer.rb
@@ -652,6 +654,9 @@ files:
652
654
  - lib/new_relic/agent/opentelemetry/translators/generic_translator.rb
653
655
  - lib/new_relic/agent/opentelemetry/translators/http_client_translator.rb
654
656
  - lib/new_relic/agent/opentelemetry/translators/http_server_translator.rb
657
+ - lib/new_relic/agent/opentelemetry/translators/messaging_translator.rb
658
+ - lib/new_relic/agent/opentelemetry/translators/redis_datastore_translator.rb
659
+ - lib/new_relic/agent/opentelemetry/translators/rpc_translator.rb
655
660
  - lib/new_relic/agent/opentelemetry_bridge.rb
656
661
  - lib/new_relic/agent/parameter_filtering.rb
657
662
  - lib/new_relic/agent/payload_metric_mapping.rb