appsignal 4.10.2-java → 5.0.0.rc.1-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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -38
  3. data/README.md +1 -2
  4. data/Rakefile +87 -0
  5. data/appsignal.gemspec +8 -7
  6. data/build_matrix.yml +14 -7
  7. data/ext/agent.rb +27 -27
  8. data/ext/appsignal_extension.c +14 -0
  9. data/lib/appsignal/backends.rb +55 -0
  10. data/lib/appsignal/cli/demo.rb +0 -5
  11. data/lib/appsignal/cli/diagnose.rb +38 -6
  12. data/lib/appsignal/cli/helpers.rb +0 -45
  13. data/lib/appsignal/config.rb +140 -1
  14. data/lib/appsignal/demo.rb +1 -1
  15. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
  16. data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
  17. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
  18. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
  19. data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
  20. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
  21. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
  22. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
  23. data/lib/appsignal/event_formatter.rb +78 -0
  24. data/lib/appsignal/extension.rb +4 -0
  25. data/lib/appsignal/helpers/instrumentation.rb +236 -20
  26. data/lib/appsignal/helpers/metrics.rb +3 -24
  27. data/lib/appsignal/hooks/action_cable.rb +18 -6
  28. data/lib/appsignal/hooks/active_job.rb +128 -179
  29. data/lib/appsignal/hooks/at_exit.rb +4 -1
  30. data/lib/appsignal/hooks/excon.rb +20 -0
  31. data/lib/appsignal/hooks/faraday.rb +16 -0
  32. data/lib/appsignal/hooks/http.rb +5 -0
  33. data/lib/appsignal/hooks/resque.rb +1 -1
  34. data/lib/appsignal/hooks/sequel.rb +32 -2
  35. data/lib/appsignal/hooks/shoryuken.rb +3 -3
  36. data/lib/appsignal/hooks/sidekiq.rb +1 -1
  37. data/lib/appsignal/integrations/action_cable.rb +5 -2
  38. data/lib/appsignal/integrations/active_support_notifications.rb +59 -19
  39. data/lib/appsignal/integrations/data_mapper.rb +14 -2
  40. data/lib/appsignal/integrations/delayed_job_plugin.rb +64 -131
  41. data/lib/appsignal/integrations/dry_monitor.rb +39 -15
  42. data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
  43. data/lib/appsignal/integrations/excon.rb +52 -15
  44. data/lib/appsignal/integrations/faraday.rb +47 -12
  45. data/lib/appsignal/integrations/http.rb +43 -1
  46. data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
  47. data/lib/appsignal/integrations/net_http.rb +31 -2
  48. data/lib/appsignal/integrations/puma.rb +4 -1
  49. data/lib/appsignal/integrations/que.rb +224 -37
  50. data/lib/appsignal/integrations/railtie.rb +4 -1
  51. data/lib/appsignal/integrations/rake.rb +9 -3
  52. data/lib/appsignal/integrations/redis.rb +22 -1
  53. data/lib/appsignal/integrations/redis_client.rb +22 -1
  54. data/lib/appsignal/integrations/resque.rb +57 -10
  55. data/lib/appsignal/integrations/shoryuken.rb +140 -12
  56. data/lib/appsignal/integrations/sidekiq.rb +73 -16
  57. data/lib/appsignal/integrations/webmachine.rb +47 -4
  58. data/lib/appsignal/loaders/padrino.rb +2 -1
  59. data/lib/appsignal/logger/extension_backend.rb +24 -0
  60. data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
  61. data/lib/appsignal/logger.rb +13 -9
  62. data/lib/appsignal/metrics/extension_backend.rb +47 -0
  63. data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
  64. data/lib/appsignal/opentelemetry/attributes.rb +31 -0
  65. data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
  66. data/lib/appsignal/opentelemetry/error_type.rb +37 -0
  67. data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
  68. data/lib/appsignal/opentelemetry/http_method.rb +59 -0
  69. data/lib/appsignal/opentelemetry/http_response.rb +30 -0
  70. data/lib/appsignal/opentelemetry/http_server_request.rb +45 -0
  71. data/lib/appsignal/opentelemetry/messaging.rb +82 -0
  72. data/lib/appsignal/opentelemetry/rendering.rb +29 -0
  73. data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
  74. data/lib/appsignal/opentelemetry.rb +337 -0
  75. data/lib/appsignal/rack/abstract_middleware.rb +63 -4
  76. data/lib/appsignal/rack/body_wrapper.rb +18 -5
  77. data/lib/appsignal/rack/event_handler.rb +36 -3
  78. data/lib/appsignal/rack/grape_middleware.rb +8 -37
  79. data/lib/appsignal/rack/hanami_middleware.rb +2 -1
  80. data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
  81. data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
  82. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
  83. data/lib/appsignal/rack.rb +39 -11
  84. data/lib/appsignal/sample_data.rb +4 -0
  85. data/lib/appsignal/transaction/base_backend.rb +107 -0
  86. data/lib/appsignal/transaction/extension_backend.rb +203 -0
  87. data/lib/appsignal/transaction/opentelemetry_backend.rb +796 -0
  88. data/lib/appsignal/transaction.rb +537 -144
  89. data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
  90. data/lib/appsignal/version.rb +1 -1
  91. data/lib/appsignal.rb +9 -0
  92. data/sig/appsignal.rbi +426 -37
  93. data/sig/appsignal.rbs +386 -27
  94. metadata +23 -1
@@ -36,51 +36,6 @@ module Appsignal
36
36
  "\e[#{color_code}m#{text}\e[#{reset_color_code}m"
37
37
  end
38
38
 
39
- def print_empty_line
40
- puts "\n"
41
- end
42
-
43
- def rails_present?
44
- require "rails"
45
- true
46
- rescue LoadError
47
- false
48
- end
49
-
50
- # The Rails gem being loadable says nothing about the directory the
51
- # command runs in, so check for the app itself as well.
52
- def rails_app_present?
53
- rails_present? &&
54
- File.exist?(Appsignal::Utils::RailsHelper.environment_config_path)
55
- end
56
-
57
- def load_rails_app(environment)
58
- # Pass the environment given as a command line option to the app, so
59
- # that the AppSignal config file uses it when the app loads it.
60
- ENV["_APPSIGNAL_CONFIG_FILE_ENV"] = environment
61
- # Require the railtie manually. It was not loaded when AppSignal
62
- # loaded, because the `Rails` constant was not present at that point.
63
- require "appsignal/integrations/railtie"
64
- # Start the Rails app, including its railties and initializers.
65
- require Appsignal::Utils::RailsHelper.environment_config_path
66
- ensure
67
- ENV.delete("_APPSIGNAL_CONFIG_FILE_ENV")
68
- end
69
-
70
- # Yields the error when the app fails to load, so that a command can
71
- # report it in its own way.
72
- def require_rails_app_if_present(environment)
73
- return unless rails_app_present?
74
-
75
- load_rails_app(environment)
76
- rescue LoadError, StandardError => error
77
- print_empty_line
78
- puts "ERROR: Error encountered while loading the Rails app"
79
- puts "#{error.class}: #{error.message}"
80
- puts error.backtrace
81
- yield error if block_given?
82
- end
83
-
84
39
  def periods
85
40
  3.times do
86
41
  print "."
@@ -91,6 +91,7 @@ module Appsignal
91
91
  DEFAULT_CONFIG = {
92
92
  :activejob_report_errors => "all",
93
93
  :ca_file_path => File.expand_path(File.join("../../../resources/cacert.pem"), __FILE__),
94
+ :collector_endpoint => nil,
94
95
  :dns_servers => [],
95
96
  :enable_allocation_tracking => true,
96
97
  :enable_at_exit_hook => "on_error",
@@ -107,8 +108,12 @@ module Appsignal
107
108
  :enable_rake_performance_instrumentation => false,
108
109
  :endpoint => "https://push.appsignal.com",
109
110
  :files_world_accessible => true,
111
+ :filter_attributes => [],
112
+ :filter_function_parameters => [],
110
113
  :filter_metadata => [],
111
114
  :filter_parameters => [],
115
+ :filter_request_payload => [],
116
+ :filter_request_query_parameters => [],
112
117
  :filter_session_data => [],
113
118
  :ignore_actions => [],
114
119
  :ignore_errors => [],
@@ -139,9 +144,14 @@ module Appsignal
139
144
  REQUEST_METHOD REQUEST_PATH SERVER_NAME SERVER_PORT
140
145
  SERVER_PROTOCOL
141
146
  ],
147
+ :response_headers => [],
142
148
  :send_environment_metadata => true,
149
+ :send_function_parameters => nil,
143
150
  :send_params => true,
151
+ :send_request_payload => nil,
152
+ :send_request_query_parameters => nil,
144
153
  :send_session_data => true,
154
+ :service_name => nil,
145
155
  :sidekiq_report_errors => "all",
146
156
  :default_tags => {}
147
157
  }.freeze
@@ -167,6 +177,7 @@ module Appsignal
167
177
  :name => "APPSIGNAL_APP_NAME",
168
178
  :bind_address => "APPSIGNAL_BIND_ADDRESS",
169
179
  :ca_file_path => "APPSIGNAL_CA_FILE_PATH",
180
+ :collector_endpoint => "APPSIGNAL_COLLECTOR_ENDPOINT",
170
181
  :enable_at_exit_hook => "APPSIGNAL_ENABLE_AT_EXIT_HOOK",
171
182
  :hostname => "APPSIGNAL_HOSTNAME",
172
183
  :host_role => "APPSIGNAL_HOST_ROLE",
@@ -177,6 +188,7 @@ module Appsignal
177
188
  :logging_endpoint => "APPSIGNAL_LOGGING_ENDPOINT",
178
189
  :endpoint => "APPSIGNAL_PUSH_API_ENDPOINT",
179
190
  :push_api_key => "APPSIGNAL_PUSH_API_KEY",
191
+ :service_name => "APPSIGNAL_SERVICE_NAME",
180
192
  :sidekiq_report_errors => "APPSIGNAL_SIDEKIQ_REPORT_ERRORS",
181
193
  :statsd_port => "APPSIGNAL_STATSD_PORT",
182
194
  :nginx_port => "APPSIGNAL_NGINX_PORT",
@@ -221,21 +233,29 @@ module Appsignal
221
233
  :ownership_set_namespace => "APPSIGNAL_OWNERSHIP_SET_NAMESPACE",
222
234
  :running_in_container => "APPSIGNAL_RUNNING_IN_CONTAINER",
223
235
  :send_environment_metadata => "APPSIGNAL_SEND_ENVIRONMENT_METADATA",
236
+ :send_function_parameters => "APPSIGNAL_SEND_FUNCTION_PARAMETERS",
224
237
  :send_params => "APPSIGNAL_SEND_PARAMS",
238
+ :send_request_payload => "APPSIGNAL_SEND_REQUEST_PAYLOAD",
239
+ :send_request_query_parameters => "APPSIGNAL_SEND_REQUEST_QUERY_PARAMETERS",
225
240
  :send_session_data => "APPSIGNAL_SEND_SESSION_DATA"
226
241
  }.freeze
227
242
 
228
243
  # @!visibility private
229
244
  ARRAY_OPTIONS = {
230
245
  :dns_servers => "APPSIGNAL_DNS_SERVERS",
246
+ :filter_attributes => "APPSIGNAL_FILTER_ATTRIBUTES",
247
+ :filter_function_parameters => "APPSIGNAL_FILTER_FUNCTION_PARAMETERS",
231
248
  :filter_metadata => "APPSIGNAL_FILTER_METADATA",
232
249
  :filter_parameters => "APPSIGNAL_FILTER_PARAMETERS",
250
+ :filter_request_payload => "APPSIGNAL_FILTER_REQUEST_PAYLOAD",
251
+ :filter_request_query_parameters => "APPSIGNAL_FILTER_REQUEST_QUERY_PARAMETERS",
233
252
  :filter_session_data => "APPSIGNAL_FILTER_SESSION_DATA",
234
253
  :ignore_actions => "APPSIGNAL_IGNORE_ACTIONS",
235
254
  :ignore_errors => "APPSIGNAL_IGNORE_ERRORS",
236
255
  :ignore_logs => "APPSIGNAL_IGNORE_LOGS",
237
256
  :ignore_namespaces => "APPSIGNAL_IGNORE_NAMESPACES",
238
- :request_headers => "APPSIGNAL_REQUEST_HEADERS"
257
+ :request_headers => "APPSIGNAL_REQUEST_HEADERS",
258
+ :response_headers => "APPSIGNAL_RESPONSE_HEADERS"
239
259
  }.freeze
240
260
 
241
261
  # @!visibility private
@@ -248,6 +268,39 @@ module Appsignal
248
268
  :default_tags => "APPSIGNAL_DEFAULT_TAGS"
249
269
  }.freeze
250
270
 
271
+ # Collector mode requires Ruby 3.1+. The OpenTelemetry Ruby SDK relies on
272
+ # `Process._fork` (introduced in Ruby 3.1) for its fork hooks, without
273
+ # which background reader threads don't restart in child processes and
274
+ # buffered telemetry is lost after a fork.
275
+ # @!visibility private
276
+ MIN_RUBY_VERSION_FOR_COLLECTOR_MODE = "3.1"
277
+
278
+ # Configuration options that only have an effect when the integration is
279
+ # in collector mode. When the agent is in use, setting any of these emits
280
+ # a warning at startup.
281
+ # @!visibility private
282
+ COLLECTOR_ONLY_OPTIONS = [
283
+ :filter_attributes,
284
+ :filter_function_parameters,
285
+ :filter_request_payload,
286
+ :filter_request_query_parameters,
287
+ :response_headers,
288
+ :send_function_parameters,
289
+ :send_request_payload,
290
+ :send_request_query_parameters,
291
+ :service_name
292
+ ].freeze
293
+
294
+ # Existing AppSignal options that only affect the agent's handling of
295
+ # trace data. In collector mode these don't filter anything; users need
296
+ # their collector-mode equivalents (see COLLECTOR_ONLY_OPTIONS).
297
+ # @!visibility private
298
+ AGENT_ONLY_TRACE_OPTIONS = [
299
+ :filter_metadata,
300
+ :filter_parameters,
301
+ :send_params
302
+ ].freeze
303
+
251
304
  # @!visibility private
252
305
  attr_reader :root_path, :env, :config_hash
253
306
 
@@ -452,6 +505,57 @@ module Appsignal
452
505
  valid? && active_for_env?
453
506
  end
454
507
 
508
+ # Check if collector mode is configured.
509
+ #
510
+ # Returns true when a non-empty `collector_endpoint` is set and the
511
+ # running Ruby version is at least {MIN_RUBY_VERSION_FOR_COLLECTOR_MODE}.
512
+ # On older Rubies, `collector_endpoint` is ignored (with a warning) and
513
+ # the AppSignal agent is used instead.
514
+ #
515
+ # This is the *intent* check — it answers "did the user ask for
516
+ # collector mode, and could we honor it?". It does not say whether the
517
+ # OpenTelemetry SDK actually booted. See {#collector_mode?} for that.
518
+ #
519
+ # Memoised: the result is cached on first call so hot paths avoid
520
+ # re-running the string-strip predicate, and so the unsupported-Ruby
521
+ # warning is emitted at most once per `Config` instance.
522
+ #
523
+ # @return [Boolean] True if collector mode is configured.
524
+ def collector_mode_configured?
525
+ return @collector_mode_configured if defined?(@collector_mode_configured)
526
+
527
+ endpoint = config_hash[:collector_endpoint]
528
+ configured = !endpoint.nil? && !endpoint.to_s.strip.empty?
529
+
530
+ if configured && Gem::Version.new(RUBY_VERSION) <
531
+ Gem::Version.new(MIN_RUBY_VERSION_FOR_COLLECTOR_MODE)
532
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
533
+ "Collector mode requires Ruby #{MIN_RUBY_VERSION_FOR_COLLECTOR_MODE} or higher " \
534
+ "(running Ruby #{RUBY_VERSION}). The `collector_endpoint` option will be " \
535
+ "ignored and the AppSignal agent will be used instead."
536
+ )
537
+ @collector_mode_configured = false
538
+ else
539
+ @collector_mode_configured = configured
540
+ end
541
+ end
542
+
543
+ # Check if AppSignal is actively running in collector mode.
544
+ #
545
+ # True only if collector mode is {#collector_mode_configured? configured}
546
+ # *and* `Appsignal::OpenTelemetry.configure` has successfully booted the
547
+ # SDK in this process. Use this for backend dispatch on hot paths
548
+ # (metric and log emits): if the OTel boot failed, callers fall back to
549
+ # the agent backend rather than silently dropping data into no-op
550
+ # providers.
551
+ #
552
+ # @return [Boolean] True if collector mode is configured and started.
553
+ def collector_mode?
554
+ collector_mode_configured? &&
555
+ defined?(Appsignal::OpenTelemetry) &&
556
+ Appsignal::OpenTelemetry.started?
557
+ end
558
+
455
559
  # @!visibility private
456
560
  def write_to_environment
457
561
  ENV["_APPSIGNAL_ACTIVE"] = active?.to_s
@@ -528,6 +632,30 @@ module Appsignal
528
632
  else
529
633
  @valid = true
530
634
  end
635
+
636
+ warn_for_mode_mismatch
637
+ end
638
+
639
+ # Emit warnings when a configuration option is set that has no effect in
640
+ # the current mode (collector vs. agent).
641
+ #
642
+ # Uses {#collector_mode_configured?} (intent) rather than
643
+ # {#collector_mode?} so the warnings fire based on what the user asked
644
+ # for, independent of whether the OpenTelemetry SDK successfully booted.
645
+ # @!visibility private
646
+ def warn_for_mode_mismatch
647
+ if collector_mode_configured?
648
+ warn_user_modified(AGENT_ONLY_TRACE_OPTIONS) do |option|
649
+ "The collector is in use. The '#{option}' configuration option is " \
650
+ "only used by the agent for trace data and will be ignored."
651
+ end
652
+ else
653
+ warn_user_modified(COLLECTOR_ONLY_OPTIONS) do |option|
654
+ "The agent is in use. The '#{option}' configuration option is " \
655
+ "only used by the collector and will be ignored. Set " \
656
+ "'collector_endpoint' to use the collector."
657
+ end
658
+ end
531
659
  end
532
660
 
533
661
  # Deep freeze the config object so it cannot be modified during the runtime
@@ -551,6 +679,17 @@ module Appsignal
551
679
 
552
680
  private
553
681
 
682
+ # Yield a warning for each option in `options` whose effective value
683
+ # differs from the default. Setting an option to its default value is
684
+ # a no-op, so we don't warn about it.
685
+ def warn_user_modified(options)
686
+ options.each do |option|
687
+ next if config_hash[option] == DEFAULT_CONFIG[option]
688
+
689
+ logger.warn(yield(option))
690
+ end
691
+ end
692
+
554
693
  def logger
555
694
  Appsignal.internal_logger
556
695
  end
@@ -70,7 +70,7 @@ module Appsignal
70
70
  end
71
71
 
72
72
  def add_params_to(transaction)
73
- transaction.add_params(
73
+ transaction.add_request_payload(
74
74
  "controller" => "demo",
75
75
  "action" => "hello"
76
76
  )
@@ -7,13 +7,27 @@ module Appsignal
7
7
  class RenderFormatter < Appsignal::EventFormatter
8
8
  BLANK = ""
9
9
 
10
+ def opentelemetry_attributes(_payload)
11
+ Appsignal::OpenTelemetry::Rendering.attributes
12
+ end
13
+
10
14
  def format(payload)
11
- return nil unless payload[:identifier]
15
+ # The title is the template's path made relative to the application's
16
+ # root, so a template rendered outside an application gets no title.
17
+ return unless payload[:identifier] && root_path
12
18
 
13
19
  [payload[:identifier].sub(root_path, BLANK), nil]
14
20
  end
15
21
 
22
+ # The application's root, which a template's path is made relative to.
23
+ #
24
+ # Whether there is an application is decided here, when the event is
25
+ # formatted, rather than when this file is loaded. AppSignal can be
26
+ # required before Rails is, and deciding it at load time would leave
27
+ # every template render in the application without a title.
16
28
  def root_path
29
+ return unless defined?(Rails)
30
+
17
31
  @root_path ||= "#{Rails.root}/"
18
32
  end
19
33
  end
@@ -21,24 +35,22 @@ module Appsignal
21
35
  end
22
36
  end
23
37
 
24
- if defined?(Rails)
25
- Appsignal::EventFormatter.register(
26
- "render_partial.action_view",
27
- Appsignal::EventFormatter::ActionView::RenderFormatter
28
- )
29
- Appsignal::EventFormatter.register(
30
- "render_template.action_view",
31
- Appsignal::EventFormatter::ActionView::RenderFormatter
32
- )
33
- # Action View reports the template's path for these two as well, so they are
34
- # titled the same way. A collection reports the partial it rendered for each
35
- # item, and a layout reports itself.
36
- Appsignal::EventFormatter.register(
37
- "render_collection.action_view",
38
- Appsignal::EventFormatter::ActionView::RenderFormatter
39
- )
40
- Appsignal::EventFormatter.register(
41
- "render_layout.action_view",
42
- Appsignal::EventFormatter::ActionView::RenderFormatter
43
- )
44
- end
38
+ Appsignal::EventFormatter.register(
39
+ "render_partial.action_view",
40
+ Appsignal::EventFormatter::ActionView::RenderFormatter
41
+ )
42
+ Appsignal::EventFormatter.register(
43
+ "render_template.action_view",
44
+ Appsignal::EventFormatter::ActionView::RenderFormatter
45
+ )
46
+ # Action View reports the template's path for these two as well, so they are
47
+ # titled the same way. A collection reports the partial it rendered for each
48
+ # item, and a layout reports itself.
49
+ Appsignal::EventFormatter.register(
50
+ "render_collection.action_view",
51
+ Appsignal::EventFormatter::ActionView::RenderFormatter
52
+ )
53
+ Appsignal::EventFormatter.register(
54
+ "render_layout.action_view",
55
+ Appsignal::EventFormatter::ActionView::RenderFormatter
56
+ )
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class EventFormatter
5
+ # @!visibility private
6
+ module ActiveJob
7
+ # Active Job reports the job it is running as a `perform.active_job`
8
+ # notification. The job's own transaction already carries its title, so
9
+ # this formatter only describes the event as the work of performing a
10
+ # job.
11
+ class PerformFormatter < Appsignal::EventFormatter
12
+ PERFORM_ATTRIBUTES =
13
+ Appsignal::OpenTelemetry::Messaging.perform_attributes("active_job").freeze
14
+
15
+ def opentelemetry_attributes(payload)
16
+ PERFORM_ATTRIBUTES.merge(
17
+ { "messaging.destination.name" => queue_name(payload) }.compact
18
+ )
19
+ end
20
+
21
+ # The queue the job being performed is on, which the notification
22
+ # carries as the job itself.
23
+ def queue_name(payload)
24
+ job = payload[:job]
25
+ job.queue_name if job.respond_to?(:queue_name)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ Appsignal::EventFormatter.register(
33
+ "perform.active_job",
34
+ Appsignal::EventFormatter::ActiveJob::PerformFormatter
35
+ )
@@ -5,6 +5,25 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module ActiveRecord
7
7
  class SqlFormatter < Appsignal::EventFormatter
8
+ # A query is an outgoing call to a datastore.
9
+ def opentelemetry_kind
10
+ :client
11
+ end
12
+
13
+ # The payload carries the connection the query ran on (Rails 6.0+),
14
+ # whose `adapter_name` is each adapter's own name for itself, such as
15
+ # `"PostgreSQL"` or `"Mysql2"`. A name the mapping does not recognise
16
+ # is left to the SQL sentinel, same as an adapter this gem has never
17
+ # heard of.
18
+ def opentelemetry_attributes(payload)
19
+ name = Appsignal::OpenTelemetry::SqlDbSystem.name_for_active_record(
20
+ payload[:connection]&.adapter_name
21
+ )
22
+ return unless name
23
+
24
+ { "db.system.name" => name }
25
+ end
26
+
8
27
  def format(payload)
9
28
  [payload[:name], payload[:sql], SQL_BODY_FORMAT]
10
29
  end
@@ -5,6 +5,21 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module ElasticSearch
7
7
  class SearchFormatter < Appsignal::EventFormatter
8
+ # A search is an outgoing call to an Elasticsearch cluster.
9
+ def opentelemetry_kind
10
+ :client
11
+ end
12
+
13
+ def opentelemetry_attributes(payload)
14
+ {
15
+ "db.system.name" => "elasticsearch",
16
+ # This notification is only emitted for a search, so that is the
17
+ # operation every one of these spans describes.
18
+ "db.operation.name" => "search",
19
+ "db.collection.name" => search_index(payload)
20
+ }.compact
21
+ end
22
+
8
23
  def format(payload)
9
24
  [
10
25
  "#{payload[:name]}: #{payload[:klass]}",
@@ -12,6 +27,18 @@ module Appsignal
12
27
  ]
13
28
  end
14
29
 
30
+ # The index a search ran against, which the notification carries in the
31
+ # search it describes. A search that names more than one index, or none
32
+ # at all, is left without this attribute rather than described with a
33
+ # value that is not an index name.
34
+ def search_index(payload)
35
+ search = payload[:search]
36
+ return unless search.respond_to?(:[])
37
+
38
+ index = search[:index]
39
+ index if index.is_a?(String)
40
+ end
41
+
15
42
  def sanitized_search(search)
16
43
  return unless search.is_a?(Hash)
17
44
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class EventFormatter
5
+ # Registered for an event that a dedicated AppSignal integration already
6
+ # records with richer semantics. The generic instrumentation paths ask the
7
+ # registry whether to record an event, so registering this for an event
8
+ # name is how that integration claims it.
9
+ #
10
+ # @!visibility private
11
+ class RecordedElsewhere < Appsignal::EventFormatter
12
+ def record?
13
+ false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,6 +5,18 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module Rom
7
7
  class SqlFormatter < Appsignal::EventFormatter
8
+ # These events arrive over dry-monitor, which is a notification bus
9
+ # rather than a library that queries a database. ROM is what emits
10
+ # them, so that is what the scope names.
11
+ def opentelemetry_scope
12
+ ["appsignal-ruby/rom", Appsignal::VERSION]
13
+ end
14
+
15
+ # A query is an outgoing call to a datastore.
16
+ def opentelemetry_kind
17
+ :client
18
+ end
19
+
8
20
  # dry-monitor reports an event under an id rather than a name, so the
9
21
  # first value here names the event. Naming it after ROM keeps every ROM
10
22
  # query in one group.
@@ -17,6 +29,18 @@ module Appsignal
17
29
  def format(payload)
18
30
  ["query.rom", payload[:query], SQL_BODY_FORMAT]
19
31
  end
32
+
33
+ # The payload's `name` is Sequel's `database_type` symbol for the
34
+ # database ROM is talking to, so this uses Sequel's own lookup
35
+ # rather than a coincidentally similar one. A symbol the mapping does
36
+ # not recognise is left to the SQL sentinel, same as an engine this
37
+ # gem has never heard of.
38
+ def opentelemetry_attributes(payload)
39
+ name = Appsignal::OpenTelemetry::SqlDbSystem.name_for_sequel(payload[:name])
40
+ return unless name
41
+
42
+ { "db.system.name" => name }
43
+ end
20
44
  end
21
45
  end
22
46
  end
@@ -10,6 +10,11 @@ module Appsignal
10
10
  # formatter the sequel-rails events are recorded without the SQL query
11
11
  # that's being executed.
12
12
  class SqlFormatter < Appsignal::EventFormatter
13
+ # A query is an outgoing call to a datastore.
14
+ def opentelemetry_kind
15
+ :client
16
+ end
17
+
13
18
  def format(payload)
14
19
  [payload[:name].to_s, payload[:sql], SQL_BODY_FORMAT]
15
20
  end
@@ -7,11 +7,28 @@ module Appsignal
7
7
  class RenderFormatter < Appsignal::EventFormatter
8
8
  BLANK = ""
9
9
 
10
+ def opentelemetry_attributes(_payload)
11
+ Appsignal::OpenTelemetry::Rendering.attributes
12
+ end
13
+
10
14
  def format(payload)
15
+ # The body is the component's path made relative to the application's
16
+ # root, so a component rendered outside an application gets no title
17
+ # and no body.
18
+ return unless root_path
19
+
11
20
  [payload[:name], payload[:identifier].sub(root_path, BLANK)]
12
21
  end
13
22
 
23
+ # The application's root, which a component's path is made relative to.
24
+ #
25
+ # Whether there is an application is decided here, when the event is
26
+ # formatted, rather than when this file is loaded. AppSignal can be
27
+ # required before Rails is, and deciding it at load time would leave
28
+ # every component render in the application without a title.
14
29
  def root_path
30
+ return unless defined?(Rails)
31
+
15
32
  @root_path ||= "#{Rails.root}/"
16
33
  end
17
34
  end
@@ -19,13 +36,7 @@ module Appsignal
19
36
  end
20
37
  end
21
38
 
22
- if defined?(Rails)
23
- Appsignal::EventFormatter.register(
24
- "render.view_component",
25
- Appsignal::EventFormatter::ViewComponent::RenderFormatter
26
- )
27
- Appsignal::EventFormatter.register(
28
- "!render.view_component",
29
- Appsignal::EventFormatter::ViewComponent::RenderFormatter
30
- )
31
- end
39
+ Appsignal::EventFormatter.register(
40
+ "render.view_component",
41
+ Appsignal::EventFormatter::ViewComponent::RenderFormatter
42
+ )
@@ -102,8 +102,77 @@ module Appsignal
102
102
  formatter&.format(payload)
103
103
  end
104
104
 
105
+ # The OpenTelemetry span kind for an event, which its formatter can
106
+ # declare. An event with no formatter, or whose formatter declares
107
+ # nothing, has no kind of its own and falls back to the default.
108
+ #
109
+ # A formatter written against the documented interface only implements
110
+ # `format`, so ask whether this one answers to the method at all rather
111
+ # than assuming every formatter does.
112
+ #
113
+ # @!visibility private
114
+ def opentelemetry_kind(name)
115
+ formatter = formatter_for(name)
116
+ return unless formatter.respond_to?(:opentelemetry_kind)
117
+
118
+ formatter.opentelemetry_kind
119
+ end
120
+
121
+ # The OpenTelemetry attributes describing an event, which its formatter
122
+ # can build from the event's payload. An event with no formatter, or
123
+ # whose formatter describes nothing, gets no attributes of its own.
124
+ #
125
+ # @!visibility private
126
+ def opentelemetry_attributes(name, payload)
127
+ formatter = formatter_for(name)
128
+ return unless formatter.respond_to?(:opentelemetry_attributes)
129
+
130
+ formatter.opentelemetry_attributes(payload)
131
+ end
132
+
133
+ # The OpenTelemetry instrumentation scope for an event, which its
134
+ # formatter can declare. It names the library the instrumentation is for,
135
+ # which is not always the library the event arrived through.
136
+ #
137
+ # An event with no formatter, or whose formatter declares nothing, is
138
+ # left to the scope the recording path derives for it.
139
+ #
140
+ # @!visibility private
141
+ def opentelemetry_scope(name)
142
+ formatter = formatter_for(name)
143
+ return unless formatter.respond_to?(:opentelemetry_scope)
144
+
145
+ formatter.opentelemetry_scope
146
+ end
147
+
148
+ # Whether the generic instrumentation paths should record an event, which
149
+ # its formatter can answer. An event a dedicated integration already
150
+ # records says no, so that it is not recorded a second time. An event
151
+ # with no formatter, or whose formatter says nothing, is recorded.
152
+ #
153
+ # @!visibility private
154
+ def record?(name)
155
+ formatter = formatter_for(name)
156
+ return true unless formatter.respond_to?(:record?)
157
+
158
+ formatter.record?
159
+ end
160
+
105
161
  private
106
162
 
163
+ # The formatter registered for an event name.
164
+ #
165
+ # A formatter is registered under whatever key was given to `register`,
166
+ # which is a String for every formatter in this gem. An event can be
167
+ # instrumented under a Symbol name, so fall back to the String form of it.
168
+ #
169
+ # `format` does not do this, on purpose. It has always looked a name up
170
+ # exactly as given, so making it match a Symbol name would start giving a
171
+ # title to events that have never had one.
172
+ def formatter_for(name)
173
+ formatters[name] || formatters[name.to_s]
174
+ end
175
+
107
176
  def initialize_formatter(name, formatter)
108
177
  format_method = formatter.instance_method(:format)
109
178
  if !format_method || format_method.arity != 1
@@ -123,6 +192,15 @@ module Appsignal
123
192
  end
124
193
  end
125
194
 
195
+ # The title and the body to show for an event, as an array. A formatter
196
+ # that is registered to describe an event in some other way, rather than to
197
+ # name it, does not have to implement this.
198
+ #
199
+ # @!visibility private
200
+ def format(_payload)
201
+ nil
202
+ end
203
+
126
204
  # @return [Integer]
127
205
  # @api public
128
206
  DEFAULT = 0