appsignal 5.0.0.rc.1-java → 5.0.0.rc.2-java

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +140 -0
  3. data/README.md +2 -1
  4. data/Rakefile +110 -6
  5. data/appsignal.gemspec +7 -0
  6. data/build_matrix.yml +7 -2
  7. data/ext/agent.rb +27 -27
  8. data/lib/appsignal/cli/demo.rb +5 -0
  9. data/lib/appsignal/cli/diagnose.rb +8 -48
  10. data/lib/appsignal/cli/helpers.rb +45 -0
  11. data/lib/appsignal/config.rb +262 -37
  12. data/lib/appsignal/demo.rb +11 -9
  13. data/lib/appsignal/helpers/instrumentation.rb +88 -0
  14. data/lib/appsignal/hooks/action_cable.rb +8 -2
  15. data/lib/appsignal/hooks/active_job.rb +189 -1
  16. data/lib/appsignal/integrations/delayed_job_plugin.rb +172 -32
  17. data/lib/appsignal/integrations/que.rb +41 -9
  18. data/lib/appsignal/integrations/railtie.rb +4 -2
  19. data/lib/appsignal/integrations/resque.rb +26 -3
  20. data/lib/appsignal/integrations/shoryuken.rb +21 -2
  21. data/lib/appsignal/integrations/sidekiq.rb +23 -2
  22. data/lib/appsignal/integrations/webmachine.rb +13 -5
  23. data/lib/appsignal/opentelemetry/http_server_request.rb +35 -1
  24. data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
  25. data/lib/appsignal/opentelemetry.rb +182 -24
  26. data/lib/appsignal/rack/abstract_middleware.rb +4 -1
  27. data/lib/appsignal/rack/event_handler.rb +9 -2
  28. data/lib/appsignal/rack/grape_middleware.rb +37 -7
  29. data/lib/appsignal/rack.rb +29 -1
  30. data/lib/appsignal/transaction/base_backend.rb +21 -0
  31. data/lib/appsignal/transaction/extension_backend.rb +26 -0
  32. data/lib/appsignal/transaction/opentelemetry_backend.rb +90 -39
  33. data/lib/appsignal/transaction.rb +189 -32
  34. data/lib/appsignal/utils/request_headers.rb +78 -0
  35. data/lib/appsignal/utils.rb +1 -0
  36. data/lib/appsignal/version.rb +1 -1
  37. data/lib/appsignal.rb +1 -0
  38. data/sig/appsignal.rbi +205 -1
  39. data/sig/appsignal.rbs +196 -0
  40. metadata +3 -1
@@ -16,26 +16,56 @@ module Appsignal
16
16
 
17
17
  def add_transaction_metadata_after(transaction, request)
18
18
  endpoint = request.env["api.endpoint"]
19
- unless endpoint&.options
19
+ request_method, klass, path = endpoint && endpoint_action(endpoint)
20
+ unless path
20
21
  super
21
22
  return
22
23
  end
23
24
 
25
+ transaction.set_action_if_nil("#{request_method}::#{klass}##{path}")
26
+
27
+ super
28
+
29
+ transaction.set_metadata("path", path)
30
+ end
31
+
32
+ # Returns the HTTP method, API class and path that make up the action
33
+ # name, or nil when the endpoint does not describe a route.
34
+ def endpoint_action(endpoint)
24
35
  options = endpoint.options
25
- request_method = options[:method].first.to_s.upcase
26
- klass = options[:for]
36
+ return unless options
37
+
38
+ if options.key?(:path)
39
+ # Only Grape 3 and older populate `options[:path]`. Their route is
40
+ # readable too, but its path is the full route template, so reading
41
+ # it would rename the actions these applications already report.
42
+ endpoint_action_from_options(endpoint, options)
43
+ else
44
+ # Grape 4 keeps these three in a value object behind a protected
45
+ # reader, leaving the route as the only public source.
46
+ endpoint_action_from_route(endpoint)
47
+ end
48
+ end
49
+
50
+ def endpoint_action_from_options(endpoint, options)
27
51
  namespace = endpoint.namespace
28
52
  namespace = "" if namespace == "/"
29
53
 
30
54
  path = options[:path].first.to_s
31
55
  path = "/#{path}" if path[0] != "/"
32
- path = "#{namespace}#{path}"
33
56
 
34
- transaction.set_action_if_nil("#{request_method}::#{klass}##{path}")
57
+ [
58
+ options[:method].first.to_s.upcase,
59
+ options[:for],
60
+ "#{namespace}#{path}"
61
+ ]
62
+ end
35
63
 
36
- super
64
+ def endpoint_action_from_route(endpoint)
65
+ route = endpoint.routes.first
66
+ return unless route
37
67
 
38
- transaction.set_metadata("path", path)
68
+ [route.request_method.to_s.upcase, endpoint.api, route.origin]
39
69
  end
40
70
  end
41
71
  end
@@ -47,6 +47,32 @@ module Appsignal
47
47
  nil
48
48
  end
49
49
 
50
+ # Fetch a value from the request environment, for the values that are only
51
+ # available there.
52
+ #
53
+ # The request class is configurable, so it may have no environment at all,
54
+ # which is not worth logging about. Reading from one can still raise, and
55
+ # that is logged. Either way the caller gets nil and skips whatever it
56
+ # needed the value for.
57
+ #
58
+ # @param request [Rack::Request] Request object.
59
+ # @param key [String] Name of the environment key to read.
60
+ # @return [Object, NilClass]
61
+ def self.request_env_value_from(request, key)
62
+ return unless request.respond_to?(:env)
63
+
64
+ env = request.env
65
+ return unless env
66
+
67
+ env[key]
68
+ rescue => error
69
+ Appsignal.internal_logger.error(
70
+ "Exception while fetching the HTTP request environment #{key}: " \
71
+ "#{error.class}: #{error}"
72
+ )
73
+ nil
74
+ end
75
+
50
76
  # Fetch the queue start time from the request environment.
51
77
  #
52
78
  # @since 3.11.0
@@ -99,9 +125,11 @@ module Appsignal
99
125
 
100
126
  transaction.add_request_payload { params_for(request) }
101
127
  transaction.add_session_data { session_data_for(request) }
102
- transaction.add_headers do
128
+ headers, environment = Appsignal::Utils::RequestHeaders.split_lazily do
103
129
  request.env if request.respond_to?(:env)
104
130
  end
131
+ transaction.add_request_headers(&headers)
132
+ transaction.add_request_environment(&environment)
105
133
 
106
134
  queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
107
135
  transaction.set_queue_start(queue_start) if queue_start
@@ -59,6 +59,27 @@ module Appsignal
59
59
  raise NotImplementedError
60
60
  end
61
61
 
62
+ # Maps each params bucket to the configuration options that decide what
63
+ # it reports: `:filter`, naming the keys to filter out of it, and
64
+ # `:send`, deciding whether to report it at all.
65
+ def params_options
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # Maps each logical header channel (`:request_headers`,
70
+ # `:request_environment`) to the storage bucket it lands in and the
71
+ # transform to apply to each key and value added on it, as
72
+ # `[bucket, transform]`. A `nil` transform leaves the value alone.
73
+ def headers_mapping
74
+ raise NotImplementedError
75
+ end
76
+
77
+ # Maps each header bucket to the configuration option that lists the
78
+ # keys to keep in it. An option holding `nil` keeps every key.
79
+ def headers_allowlist
80
+ raise NotImplementedError
81
+ end
82
+
62
83
  # Sample data (params, session, tags, ...), breadcrumbs and errors.
63
84
  def set_sample_data(_key, _data)
64
85
  raise NotImplementedError
@@ -96,6 +96,32 @@ module Appsignal
96
96
  PARAMS_MAPPING
97
97
  end
98
98
 
99
+ PARAMS_OPTIONS = {
100
+ :params => { :filter => :filter_parameters, :send => :send_params }
101
+ }.freeze
102
+
103
+ def params_options
104
+ PARAMS_OPTIONS
105
+ end
106
+
107
+ HEADERS_MAPPING = {
108
+ :request_headers => [
109
+ :environment,
110
+ lambda { |name, value| [Appsignal::Utils::RequestHeaders.rack_name(name), value] }
111
+ ],
112
+ :request_environment => [:environment, nil]
113
+ }.freeze
114
+
115
+ def headers_mapping
116
+ HEADERS_MAPPING
117
+ end
118
+
119
+ HEADERS_ALLOWLIST = { :environment => [:request_headers, false] }.freeze
120
+
121
+ def headers_allowlist
122
+ HEADERS_ALLOWLIST
123
+ end
124
+
99
125
  # `data` is a raw Ruby Hash/Array; the C extension wants a `Data` object,
100
126
  # so serialize it here (mirrors how `set_error` serializes its backtrace).
101
127
  def set_sample_data(key, data)
@@ -14,6 +14,12 @@ module Appsignal
14
14
  class OpenTelemetryBackend < BaseBackend
15
15
  TRACER_NAME = "appsignal-ruby"
16
16
 
17
+ # The action name given to a transaction that recorded an error without
18
+ # ever setting one. Bracketed so it reads as a marker rather than as a
19
+ # class the application defines, and so it sorts apart from real action
20
+ # names.
21
+ UNNAMED_ACTION = "[unnamed action]"
22
+
17
23
  # Guards the process-wide warn-once state, which transactions touch
18
24
  # concurrently on threaded servers. A constant so it is created once at
19
25
  # load time rather than lazily (which would race).
@@ -125,6 +131,7 @@ module Appsignal
125
131
  @start_time = Time.now
126
132
  @action_set = false
127
133
  @action = nil
134
+ @error_set = false
128
135
  @allocation_start = current_allocation_count
129
136
  @root_child_allocation_count = 0
130
137
 
@@ -277,6 +284,43 @@ module Appsignal
277
284
  PARAMS_MAPPING
278
285
  end
279
286
 
287
+ PARAMS_OPTIONS = {
288
+ :request_payload => {
289
+ :filter => :filter_request_payload,
290
+ :send => :send_request_payload
291
+ },
292
+ :function_parameters => {
293
+ :filter => :filter_function_parameters,
294
+ :send => :send_function_parameters
295
+ },
296
+ :query_parameters => {
297
+ :filter => :filter_request_query_parameters,
298
+ :send => :send_request_query_parameters
299
+ }
300
+ }.freeze
301
+
302
+ def params_options
303
+ PARAMS_OPTIONS
304
+ end
305
+
306
+ HEADERS_MAPPING = {
307
+ :request_headers => [:request_headers, nil],
308
+ :request_environment => [:environment, nil]
309
+ }.freeze
310
+
311
+ def headers_mapping
312
+ HEADERS_MAPPING
313
+ end
314
+
315
+ HEADERS_ALLOWLIST = {
316
+ :request_headers => [:keep_request_headers, true],
317
+ :environment => [:keep_request_environment, false]
318
+ }.freeze
319
+
320
+ def headers_allowlist
321
+ HEADERS_ALLOWLIST
322
+ end
323
+
280
324
  # Routes each sample-data category to the attribute the collector reads.
281
325
  # The params arrive on one of three channels: `request_payload` (web),
282
326
  # `function_parameters` (jobs) and `query_parameters` (a request's query
@@ -298,8 +342,10 @@ module Appsignal
298
342
  @span.set_attribute("appsignal.request.session_data", JSON.generate(data))
299
343
  when "custom_data"
300
344
  @span.set_attribute("appsignal.custom_data", JSON.generate(data))
301
- when "environment"
345
+ when "request_headers"
302
346
  write_request_headers(data)
347
+ when "environment"
348
+ write_request_environment(data)
303
349
  when "tags"
304
350
  write_tags(data)
305
351
  else
@@ -317,6 +363,7 @@ module Appsignal
317
363
  # become their own incident. Each cause carries only the part of its
318
364
  # backtrace that is not shared (see `trim_shared_tail`).
319
365
  def set_error(class_name, message, backtrace, causes, _root_cause_missing)
366
+ @error_set = true
320
367
  span = current_span
321
368
  error_lines = Array(backtrace)
322
369
 
@@ -392,12 +439,12 @@ module Appsignal
392
439
  # `teardown` sets `@completed`, so this guard also makes the body
393
440
  # idempotent across a double `complete`, and skips it on `discard`.
394
441
  unless @completed
395
- # Aggregate metrics are only emitted for a transaction that set an
396
- # action to group by. An actionless transaction is never reported in
397
- # agent mode, so it must contribute to no aggregate here either.
398
- emit_queue_duration_metric if @action_set
442
+ # Settle the action first, because the allocation count metric below
443
+ # tags by `@action`, and this is what fills it in for a transaction
444
+ # the application never named.
445
+ resolve_missing_action
446
+ emit_queue_duration_metric if should_report?
399
447
  report_allocation_count
400
- ignore_subtrace_without_action
401
448
  end
402
449
  teardown
403
450
  end
@@ -450,15 +497,38 @@ module Appsignal
450
497
  @span&.finish
451
498
  end
452
499
 
453
- # A transaction that never set an action has nothing to group by, and agent
454
- # mode does not report one at all. Collector mode cannot represent "no
455
- # action", so the subtrace is flagged for the collector to drop instead,
456
- # the same way `discard` does. The flag has to be set before `teardown`
457
- # finishes the span, because attributes set on an ended span are dropped.
458
- def ignore_subtrace_without_action
500
+ # Whether this transaction is reported at all. One that set an action is
501
+ # reported so it can be grouped under it. One that recorded an error is
502
+ # reported even without an action, because the failure is worth reporting
503
+ # even when we cannot say what failed. The agent draws the line in the
504
+ # same place: it drops a transaction only when it has neither.
505
+ def should_report?
506
+ @action_set || @error_set
507
+ end
508
+
509
+ # Decides what to do about a transaction that never set an action, which
510
+ # has nothing to group by. Collector mode cannot represent "no action", so
511
+ # it needs a name for the ones it reports and a way to drop the rest.
512
+ #
513
+ # A reported one is named after the marker. Leaving the action unset would
514
+ # not report nothing: the collector fills an empty action in from the span
515
+ # name, which here is the placeholder this backend opened the span with,
516
+ # and that reads as though it were the application's own action.
517
+ #
518
+ # The rest are noise, such as serving assets in development, so their
519
+ # subtrace is flagged for the collector to drop, the same way `discard`
520
+ # does.
521
+ #
522
+ # Both branches have to run before `teardown` finishes the span, because
523
+ # attributes set on an ended span are dropped.
524
+ def resolve_missing_action
459
525
  return if @action_set
460
526
 
461
- @span&.set_attribute("appsignal.ignore_subtrace", true)
527
+ if should_report?
528
+ set_action(UNNAMED_ACTION)
529
+ else
530
+ @span&.set_attribute("appsignal.ignore_subtrace", true)
531
+ end
462
532
  end
463
533
 
464
534
  # Emits the queue duration as a distribution metric in both the
@@ -505,7 +575,7 @@ module Appsignal
505
575
  count - @root_child_allocation_count
506
576
  )
507
577
 
508
- return unless @action_set && count.positive?
578
+ return unless should_report? && count.positive?
509
579
 
510
580
  namespace = display_namespace(@namespace)
511
581
  Appsignal::Metrics::OpenTelemetryBackend.increment_counter(
@@ -633,7 +703,7 @@ module Appsignal
633
703
  # to a plain root span, since there is nothing to parent or link to.
634
704
  def start_transaction_span(namespace, kind, relationship, opentelemetry_context)
635
705
  name = placeholder_span_name(namespace)
636
- remote = remote_span_context(opentelemetry_context)
706
+ remote = Appsignal::OpenTelemetry.remote_span_context(opentelemetry_context)
637
707
  tracer = tracer_for(@scope)
638
708
 
639
709
  # With no incoming context (or an invalid remote span) there is nothing
@@ -682,38 +752,19 @@ module Appsignal
682
752
  frames.find { |frame| !frame.include?("/lib/appsignal/") } || frames.first
683
753
  end
684
754
 
685
- # The remote parent's SpanContext from an incoming OTel context, or nil
686
- # when there is no context or the remote span is invalid -- in which case
687
- # callers fall back to a plain root span.
688
- def remote_span_context(opentelemetry_context)
689
- return unless opentelemetry_context
690
-
691
- context = ::OpenTelemetry::Trace.current_span(opentelemetry_context).context
692
- context if context.valid?
693
- end
694
-
695
755
  def display_namespace(namespace)
696
756
  DISPLAY_NAMESPACE.fetch(namespace, namespace)
697
757
  end
698
758
 
699
- # The transaction's "environment" sample data is a Rack/CGI env allowlist
700
- # mixing true HTTP headers (HTTP_*, plus CONTENT_LENGTH/CONTENT_TYPE) with
701
- # non-header CGI vars (REQUEST_METHOD, REQUEST_PATH, PATH_INFO, SERVER_*).
702
- # Only the true headers map to the OTel `http.request.header.*` convention
703
- # the collector and trace UI read, so emit those (normalized to lowercase,
704
- # dashed header names) and drop everything else.
705
759
  def write_request_headers(headers)
706
- headers.each do |key, value|
707
- name = otel_header_name(key)
708
- @span.set_attribute("http.request.header.#{name}", value.to_s) if name
760
+ headers.each do |name, value|
761
+ @span.set_attribute("http.request.header.#{name}", value.to_s)
709
762
  end
710
763
  end
711
764
 
712
- def otel_header_name(env_key)
713
- if env_key.start_with?("HTTP_")
714
- env_key.delete_prefix("HTTP_").downcase.tr("_", "-")
715
- elsif env_key.start_with?("CONTENT_")
716
- env_key.downcase.tr("_", "-")
765
+ def write_request_environment(environment)
766
+ environment.each do |key, value|
767
+ @span.set_attribute("appsignal.environment.#{key}", value.to_s)
717
768
  end
718
769
  end
719
770