appsignal 4.10.4-java → 5.0.0-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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/Rakefile +195 -4
  4. data/appsignal.gemspec +8 -0
  5. data/build_matrix.yml +12 -0
  6. data/ext/appsignal_extension.c +14 -0
  7. data/lib/appsignal/backends.rb +55 -0
  8. data/lib/appsignal/cli/diagnose.rb +2 -10
  9. data/lib/appsignal/config.rb +377 -13
  10. data/lib/appsignal/demo.rb +12 -10
  11. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
  12. data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
  13. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
  14. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
  15. data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
  16. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
  17. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
  18. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
  19. data/lib/appsignal/event_formatter.rb +78 -0
  20. data/lib/appsignal/extension.rb +4 -0
  21. data/lib/appsignal/helpers/instrumentation.rb +324 -20
  22. data/lib/appsignal/helpers/metrics.rb +3 -24
  23. data/lib/appsignal/hooks/action_cable.rb +26 -8
  24. data/lib/appsignal/hooks/active_job.rb +184 -47
  25. data/lib/appsignal/hooks/at_exit.rb +4 -1
  26. data/lib/appsignal/hooks/excon.rb +20 -0
  27. data/lib/appsignal/hooks/faraday.rb +16 -0
  28. data/lib/appsignal/hooks/http.rb +5 -0
  29. data/lib/appsignal/hooks/resque.rb +1 -1
  30. data/lib/appsignal/hooks/sequel.rb +32 -2
  31. data/lib/appsignal/hooks/shoryuken.rb +3 -3
  32. data/lib/appsignal/hooks/sidekiq.rb +1 -1
  33. data/lib/appsignal/integrations/action_cable.rb +5 -2
  34. data/lib/appsignal/integrations/active_support_notifications.rb +59 -19
  35. data/lib/appsignal/integrations/data_mapper.rb +14 -2
  36. data/lib/appsignal/integrations/delayed_job_plugin.rb +81 -8
  37. data/lib/appsignal/integrations/dry_monitor.rb +39 -15
  38. data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
  39. data/lib/appsignal/integrations/excon.rb +52 -15
  40. data/lib/appsignal/integrations/faraday.rb +47 -12
  41. data/lib/appsignal/integrations/http.rb +43 -1
  42. data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
  43. data/lib/appsignal/integrations/net_http.rb +31 -2
  44. data/lib/appsignal/integrations/puma.rb +4 -1
  45. data/lib/appsignal/integrations/que.rb +256 -37
  46. data/lib/appsignal/integrations/railtie.rb +4 -1
  47. data/lib/appsignal/integrations/rake.rb +9 -3
  48. data/lib/appsignal/integrations/redis.rb +22 -1
  49. data/lib/appsignal/integrations/redis_client.rb +22 -1
  50. data/lib/appsignal/integrations/resque.rb +81 -11
  51. data/lib/appsignal/integrations/shoryuken.rb +159 -12
  52. data/lib/appsignal/integrations/sidekiq.rb +94 -16
  53. data/lib/appsignal/integrations/webmachine.rb +56 -5
  54. data/lib/appsignal/loaders/padrino.rb +2 -1
  55. data/lib/appsignal/logger/extension_backend.rb +24 -0
  56. data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
  57. data/lib/appsignal/logger.rb +13 -9
  58. data/lib/appsignal/metrics/extension_backend.rb +47 -0
  59. data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
  60. data/lib/appsignal/opentelemetry/attributes.rb +31 -0
  61. data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
  62. data/lib/appsignal/opentelemetry/error_type.rb +37 -0
  63. data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
  64. data/lib/appsignal/opentelemetry/http_method.rb +59 -0
  65. data/lib/appsignal/opentelemetry/http_response.rb +30 -0
  66. data/lib/appsignal/opentelemetry/http_server_request.rb +79 -0
  67. data/lib/appsignal/opentelemetry/messaging.rb +82 -0
  68. data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
  69. data/lib/appsignal/opentelemetry/rendering.rb +29 -0
  70. data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
  71. data/lib/appsignal/opentelemetry.rb +495 -0
  72. data/lib/appsignal/rack/abstract_middleware.rb +66 -4
  73. data/lib/appsignal/rack/body_wrapper.rb +18 -5
  74. data/lib/appsignal/rack/event_handler.rb +44 -4
  75. data/lib/appsignal/rack/grape_middleware.rb +1 -0
  76. data/lib/appsignal/rack/hanami_middleware.rb +2 -1
  77. data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
  78. data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
  79. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
  80. data/lib/appsignal/rack.rb +68 -12
  81. data/lib/appsignal/sample_data.rb +4 -0
  82. data/lib/appsignal/transaction/base_backend.rb +128 -0
  83. data/lib/appsignal/transaction/extension_backend.rb +229 -0
  84. data/lib/appsignal/transaction/opentelemetry_backend.rb +847 -0
  85. data/lib/appsignal/transaction.rb +714 -164
  86. data/lib/appsignal/utils/request_headers.rb +78 -0
  87. data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
  88. data/lib/appsignal/utils.rb +1 -0
  89. data/lib/appsignal/version.rb +1 -1
  90. data/lib/appsignal.rb +10 -0
  91. data/sig/appsignal.rbi +630 -37
  92. data/sig/appsignal.rbs +582 -27
  93. metadata +25 -1
@@ -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 => [],
@@ -129,6 +134,11 @@ module Appsignal
129
134
  :instrument_sequel => true,
130
135
  :instrument_shoryuken => true,
131
136
  :instrument_sidekiq => true,
137
+ :keep_request_environment => [],
138
+ :keep_request_headers => %w[
139
+ accept accept-charset accept-encoding accept-language cache-control
140
+ connection content-length range
141
+ ],
132
142
  :log => "file",
133
143
  :logging_endpoint => "https://appsignal-endpoint.net",
134
144
  :ownership_set_namespace => false,
@@ -139,9 +149,14 @@ module Appsignal
139
149
  REQUEST_METHOD REQUEST_PATH SERVER_NAME SERVER_PORT
140
150
  SERVER_PROTOCOL
141
151
  ],
152
+ :response_headers => [],
142
153
  :send_environment_metadata => true,
154
+ :send_function_parameters => true,
143
155
  :send_params => true,
156
+ :send_request_payload => true,
157
+ :send_request_query_parameters => true,
144
158
  :send_session_data => true,
159
+ :service_name => nil,
145
160
  :sidekiq_report_errors => "all",
146
161
  :default_tags => {}
147
162
  }.freeze
@@ -161,12 +176,25 @@ module Appsignal
161
176
  "trace" => ::Logger::DEBUG
162
177
  }.freeze
163
178
 
179
+ # Environment variables set by deployment platforms that name the
180
+ # revision that is being deployed, in the order the agent reads them.
181
+ # The agent detects the revision this way as well, but only for the data
182
+ # it sends itself, so the gem has to do it for collector mode.
183
+ # @!visibility private
184
+ PLATFORM_REVISION_ENV_VARS = [
185
+ "HEROKU_SLUG_COMMIT",
186
+ "RENDER_GIT_COMMIT",
187
+ "KAMAL_VERSION",
188
+ "CONTAINER_VERSION" # Scalingo
189
+ ].freeze
190
+
164
191
  # @!visibility private
165
192
  STRING_OPTIONS = {
166
193
  :activejob_report_errors => "APPSIGNAL_ACTIVEJOB_REPORT_ERRORS",
167
194
  :name => "APPSIGNAL_APP_NAME",
168
195
  :bind_address => "APPSIGNAL_BIND_ADDRESS",
169
196
  :ca_file_path => "APPSIGNAL_CA_FILE_PATH",
197
+ :collector_endpoint => "APPSIGNAL_COLLECTOR_ENDPOINT",
170
198
  :enable_at_exit_hook => "APPSIGNAL_ENABLE_AT_EXIT_HOOK",
171
199
  :hostname => "APPSIGNAL_HOSTNAME",
172
200
  :host_role => "APPSIGNAL_HOST_ROLE",
@@ -177,6 +205,7 @@ module Appsignal
177
205
  :logging_endpoint => "APPSIGNAL_LOGGING_ENDPOINT",
178
206
  :endpoint => "APPSIGNAL_PUSH_API_ENDPOINT",
179
207
  :push_api_key => "APPSIGNAL_PUSH_API_KEY",
208
+ :service_name => "APPSIGNAL_SERVICE_NAME",
180
209
  :sidekiq_report_errors => "APPSIGNAL_SIDEKIQ_REPORT_ERRORS",
181
210
  :statsd_port => "APPSIGNAL_STATSD_PORT",
182
211
  :nginx_port => "APPSIGNAL_NGINX_PORT",
@@ -221,21 +250,31 @@ module Appsignal
221
250
  :ownership_set_namespace => "APPSIGNAL_OWNERSHIP_SET_NAMESPACE",
222
251
  :running_in_container => "APPSIGNAL_RUNNING_IN_CONTAINER",
223
252
  :send_environment_metadata => "APPSIGNAL_SEND_ENVIRONMENT_METADATA",
253
+ :send_function_parameters => "APPSIGNAL_SEND_FUNCTION_PARAMETERS",
224
254
  :send_params => "APPSIGNAL_SEND_PARAMS",
255
+ :send_request_payload => "APPSIGNAL_SEND_REQUEST_PAYLOAD",
256
+ :send_request_query_parameters => "APPSIGNAL_SEND_REQUEST_QUERY_PARAMETERS",
225
257
  :send_session_data => "APPSIGNAL_SEND_SESSION_DATA"
226
258
  }.freeze
227
259
 
228
260
  # @!visibility private
229
261
  ARRAY_OPTIONS = {
230
262
  :dns_servers => "APPSIGNAL_DNS_SERVERS",
263
+ :filter_attributes => "APPSIGNAL_FILTER_ATTRIBUTES",
264
+ :filter_function_parameters => "APPSIGNAL_FILTER_FUNCTION_PARAMETERS",
231
265
  :filter_metadata => "APPSIGNAL_FILTER_METADATA",
232
266
  :filter_parameters => "APPSIGNAL_FILTER_PARAMETERS",
267
+ :filter_request_payload => "APPSIGNAL_FILTER_REQUEST_PAYLOAD",
268
+ :filter_request_query_parameters => "APPSIGNAL_FILTER_REQUEST_QUERY_PARAMETERS",
233
269
  :filter_session_data => "APPSIGNAL_FILTER_SESSION_DATA",
234
270
  :ignore_actions => "APPSIGNAL_IGNORE_ACTIONS",
235
271
  :ignore_errors => "APPSIGNAL_IGNORE_ERRORS",
236
272
  :ignore_logs => "APPSIGNAL_IGNORE_LOGS",
237
273
  :ignore_namespaces => "APPSIGNAL_IGNORE_NAMESPACES",
238
- :request_headers => "APPSIGNAL_REQUEST_HEADERS"
274
+ :keep_request_environment => "APPSIGNAL_KEEP_REQUEST_ENVIRONMENT",
275
+ :keep_request_headers => "APPSIGNAL_KEEP_REQUEST_HEADERS",
276
+ :request_headers => "APPSIGNAL_REQUEST_HEADERS",
277
+ :response_headers => "APPSIGNAL_RESPONSE_HEADERS"
239
278
  }.freeze
240
279
 
241
280
  # @!visibility private
@@ -248,6 +287,67 @@ module Appsignal
248
287
  :default_tags => "APPSIGNAL_DEFAULT_TAGS"
249
288
  }.freeze
250
289
 
290
+ # Collector mode requires Ruby 3.1+. The OpenTelemetry Ruby SDK relies on
291
+ # `Process._fork` (introduced in Ruby 3.1) for its fork hooks, without
292
+ # which background reader threads don't restart in child processes and
293
+ # buffered telemetry is lost after a fork.
294
+ # @!visibility private
295
+ MIN_RUBY_VERSION_FOR_COLLECTOR_MODE = "3.1"
296
+
297
+ # @!visibility private
298
+ COLLECTOR_ONLY_OPTIONS = [
299
+ :filter_attributes,
300
+ :filter_function_parameters,
301
+ :filter_request_payload,
302
+ :filter_request_query_parameters,
303
+ :keep_request_environment,
304
+ :keep_request_headers,
305
+ :response_headers,
306
+ :send_function_parameters,
307
+ :send_request_payload,
308
+ :send_request_query_parameters,
309
+ :service_name
310
+ ].freeze
311
+
312
+ # @!visibility private
313
+ COLLECTOR_ONLY_REPLACED_OPTIONS = {
314
+ :filter_function_parameters => :filter_parameters,
315
+ :filter_request_payload => :filter_parameters,
316
+ :filter_request_query_parameters => :filter_parameters,
317
+ :keep_request_environment => :request_headers,
318
+ :keep_request_headers => :request_headers,
319
+ :send_function_parameters => :send_params,
320
+ :send_request_payload => :send_params,
321
+ :send_request_query_parameters => :send_params
322
+ }.freeze
323
+
324
+ # @!visibility private
325
+ DEPRECATED_COLLECTOR_OPTIONS = {
326
+ :filter_parameters => {
327
+ :filter_request_payload => :derived_as_is,
328
+ :filter_function_parameters => :derived_as_is,
329
+ :filter_request_query_parameters => :derived_as_is
330
+ },
331
+ :request_headers => {
332
+ :keep_request_headers => :derived_header_names,
333
+ :keep_request_environment => :derived_environment_keys
334
+ },
335
+ :send_params => {
336
+ :send_request_payload => :derived_as_is,
337
+ :send_request_query_parameters => :derived_as_is,
338
+ :send_function_parameters => :derived_as_is
339
+ }
340
+ }.freeze
341
+
342
+ # @!visibility private
343
+ SOURCE_ORDER = [
344
+ :default, :derived, :system, :loaders, :initial, :file, :env, :override,
345
+ :dsl
346
+ ].freeze
347
+
348
+ # @!visibility private
349
+ APPLICATION_SOURCES = [:initial, :file, :env, :dsl].freeze
350
+
251
351
  # @!visibility private
252
352
  attr_reader :root_path, :env, :config_hash
253
353
 
@@ -259,8 +359,23 @@ module Appsignal
259
359
  #
260
360
  # Used by the diagnose report to list which value was read from which source.
261
361
  # @!visibility private
262
- attr_reader :system_config, :loaders_config, :initial_config, :file_config,
263
- :env_config, :override_config, :dsl_config
362
+ attr_reader :derived_config, :system_config, :loaders_config,
363
+ :initial_config, :file_config, :env_config, :override_config, :dsl_config
364
+
365
+ # @!visibility private
366
+ def config_sources
367
+ {
368
+ :default => DEFAULT_CONFIG,
369
+ :derived => derived_config,
370
+ :system => system_config,
371
+ :loaders => loaders_config,
372
+ :initial => initial_config,
373
+ :file => file_config,
374
+ :env => env_config,
375
+ :override => override_config,
376
+ :dsl => dsl_config
377
+ }
378
+ end
264
379
 
265
380
  # Initialize a new AppSignal configuration object.
266
381
  #
@@ -296,6 +411,7 @@ module Appsignal
296
411
  @initial_config = {}
297
412
  @file_config = {}
298
413
  @env_config = {}
414
+ @derived_config = {}
299
415
  @override_config = {}
300
416
  @dsl_config = {} # Can be set using `Appsignal.configure`
301
417
 
@@ -452,6 +568,57 @@ module Appsignal
452
568
  valid? && active_for_env?
453
569
  end
454
570
 
571
+ # Check if collector mode is configured.
572
+ #
573
+ # Returns true when a non-empty `collector_endpoint` is set and the
574
+ # running Ruby version is at least {MIN_RUBY_VERSION_FOR_COLLECTOR_MODE}.
575
+ # On older Rubies, `collector_endpoint` is ignored (with a warning) and
576
+ # the AppSignal agent is used instead.
577
+ #
578
+ # This is the *intent* check — it answers "did the user ask for
579
+ # collector mode, and could we honor it?". It does not say whether the
580
+ # OpenTelemetry SDK actually booted. See {#collector_mode?} for that.
581
+ #
582
+ # Memoised: the result is cached on first call so hot paths avoid
583
+ # re-running the string-strip predicate, and so the unsupported-Ruby
584
+ # warning is emitted at most once per `Config` instance.
585
+ #
586
+ # @return [Boolean] True if collector mode is configured.
587
+ def collector_mode_configured?
588
+ return @collector_mode_configured if defined?(@collector_mode_configured)
589
+
590
+ endpoint = config_hash[:collector_endpoint]
591
+ configured = !endpoint.nil? && !endpoint.to_s.strip.empty?
592
+
593
+ if configured && Gem::Version.new(RUBY_VERSION) <
594
+ Gem::Version.new(MIN_RUBY_VERSION_FOR_COLLECTOR_MODE)
595
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
596
+ "Collector mode requires Ruby #{MIN_RUBY_VERSION_FOR_COLLECTOR_MODE} or higher " \
597
+ "(running Ruby #{RUBY_VERSION}). The `collector_endpoint` option will be " \
598
+ "ignored and the AppSignal agent will be used instead."
599
+ )
600
+ @collector_mode_configured = false
601
+ else
602
+ @collector_mode_configured = configured
603
+ end
604
+ end
605
+
606
+ # Check if AppSignal is actively running in collector mode.
607
+ #
608
+ # True only if collector mode is {#collector_mode_configured? configured}
609
+ # *and* `Appsignal::OpenTelemetry.configure` has successfully booted the
610
+ # SDK in this process. Use this for backend dispatch on hot paths
611
+ # (metric and log emits): if the OTel boot failed, callers fall back to
612
+ # the agent backend rather than silently dropping data into no-op
613
+ # providers.
614
+ #
615
+ # @return [Boolean] True if collector mode is configured and started.
616
+ def collector_mode?
617
+ collector_mode_configured? &&
618
+ defined?(Appsignal::OpenTelemetry) &&
619
+ Appsignal::OpenTelemetry.started?
620
+ end
621
+
455
622
  # @!visibility private
456
623
  def write_to_environment
457
624
  ENV["_APPSIGNAL_ACTIVE"] = active?.to_s
@@ -465,6 +632,10 @@ module Appsignal
465
632
  ENV["_APPSIGNAL_ENABLE_HOST_METRICS"] = config_hash[:enable_host_metrics].to_s
466
633
  ENV["_APPSIGNAL_ENABLE_STATSD"] = config_hash[:enable_statsd].to_s
467
634
  ENV["_APPSIGNAL_ENABLE_NGINX_METRICS"] = config_hash[:enable_nginx_metrics].to_s
635
+ # The gem never sends OpenTelemetry data to the agent, so it has no
636
+ # reason to listen for it. Written out rather than left to the agent's
637
+ # own default, so a value set in the environment cannot turn it on.
638
+ ENV["_APPSIGNAL_ENABLE_OPENTELEMETRY_HTTP"] = "false"
468
639
  ENV["_APPSIGNAL_APP_ENV"] = env
469
640
  ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = config_hash[:files_world_accessible].to_s
470
641
  ENV["_APPSIGNAL_FILTER_PARAMETERS"] = config_hash[:filter_parameters].join(",")
@@ -481,6 +652,7 @@ module Appsignal
481
652
  ENV["_APPSIGNAL_LOG_LEVEL"] = config_hash[:log_level]
482
653
  ENV["_APPSIGNAL_LOG_FILE_PATH"] = log_file_path.to_s if log_file_path
483
654
  ENV["_APPSIGNAL_LOGGING_ENDPOINT"] = config_hash[:logging_endpoint]
655
+ ENV["_APPSIGNAL_PLATFORM"] = config_hash[:platform].to_s
484
656
  ENV["_APPSIGNAL_PROCESS_NAME"] = $PROGRAM_NAME
485
657
  ENV["_APPSIGNAL_PUSH_API_ENDPOINT"] = config_hash[:endpoint]
486
658
  ENV["_APPSIGNAL_PUSH_API_KEY"] = config_hash[:push_api_key]
@@ -500,9 +672,11 @@ module Appsignal
500
672
  merge(options)
501
673
  end
502
674
 
503
- # Apply any overrides for invalid settings.
504
675
  # @!visibility private
505
676
  def apply_overrides
677
+ @derived_config = determine_derived
678
+ merge(derived_config)
679
+
506
680
  @override_config = determine_overrides
507
681
  merge(override_config)
508
682
  end
@@ -528,6 +702,25 @@ module Appsignal
528
702
  else
529
703
  @valid = true
530
704
  end
705
+
706
+ warn_for_mode_mismatch
707
+ end
708
+
709
+ # Emit warnings when a configuration option is set that has no effect in
710
+ # the current mode (collector vs. agent).
711
+ #
712
+ # Uses {#collector_mode_configured?} (intent) rather than
713
+ # {#collector_mode?} so the warnings fire based on what the user asked
714
+ # for, independent of whether the OpenTelemetry SDK successfully booted.
715
+ # @!visibility private
716
+ def warn_for_mode_mismatch
717
+ if collector_mode_configured?
718
+ warn_user_modified(DEPRECATED_COLLECTOR_OPTIONS.keys) do |option|
719
+ deprecated_collector_option_message(option)
720
+ end
721
+ else
722
+ warn_collector_only_options
723
+ end
531
724
  end
532
725
 
533
726
  # Deep freeze the config object so it cannot be modified during the runtime
@@ -551,6 +744,54 @@ module Appsignal
551
744
 
552
745
  private
553
746
 
747
+ def deprecated_collector_option_message(option)
748
+ replacements = DEPRECATED_COLLECTOR_OPTIONS.fetch(option).keys
749
+ message = "The collector is in use. The '#{option}' configuration " \
750
+ "option is deprecated in collector mode. It is replaced by " \
751
+ "#{quoted_option_list(replacements)}."
752
+
753
+ derived = replacements.select { |name| derived_config.key?(name) }
754
+ return message if derived.empty?
755
+
756
+ values = derived.map { |name| "\n #{name}: #{derived_config[name].inspect}" }
757
+ "#{message} Set these options to keep reporting what this application " \
758
+ "reports now:#{values.join}"
759
+ end
760
+
761
+ def quoted_option_list(names)
762
+ names = names.map { |name| "'#{name}'" }
763
+ return names.first if names.length == 1
764
+
765
+ "#{names[0..-2].join(", ")} and #{names.last}"
766
+ end
767
+
768
+ def warn_collector_only_options
769
+ options = COLLECTOR_ONLY_OPTIONS.select { |option| configured?(option) }
770
+ return if options.empty?
771
+
772
+ options.each do |option|
773
+ logger.warn(
774
+ "The agent is in use. The '#{option}' configuration option is " \
775
+ "only used by the collector and will be ignored."
776
+ )
777
+
778
+ replacement = COLLECTOR_ONLY_REPLACED_OPTIONS[option]
779
+ logger.warn("Use the '#{replacement}' option instead.") if replacement
780
+ end
781
+
782
+ logger.info(
783
+ "To use the collector, set the 'collector_endpoint' configuration option."
784
+ )
785
+ end
786
+
787
+ def warn_user_modified(options)
788
+ options.each do |option|
789
+ next unless configured?(option)
790
+
791
+ logger.warn(yield(option))
792
+ end
793
+ end
794
+
554
795
  def logger
555
796
  Appsignal.internal_logger
556
797
  end
@@ -571,13 +812,57 @@ module Appsignal
571
812
 
572
813
  hash[:enable_at_exit_hook] = "always" if Appsignal::Extension.running_in_container?
573
814
 
574
- # Set revision from REVISION file if present in project root
575
- # This helps with Capistrano and Hatchbox.io deployments
576
- revision_from_file = detect_revision_from_file
577
- hash[:revision] = revision_from_file if revision_from_file
815
+ # Set the revision from a REVISION file in the project root, which
816
+ # helps with Capistrano and Hatchbox.io deployments, or from the
817
+ # environment variable the deployment platform sets.
818
+ revision = detect_revision_from_file || detect_revision_from_platform
819
+ hash[:revision] = revision if revision
820
+
821
+ hostname = detect_hostname
822
+ hash[:hostname] = hostname if hostname
823
+
824
+ platform = detect_platform
825
+ hash[:platform] = platform if platform
578
826
  end
579
827
  end
580
828
 
829
+ # Detect the platform the application is deployed on, the way the agent
830
+ # does. The agent only detects it for the data it reports itself, so the
831
+ # gem has to do it for collector mode. There is no config option for it,
832
+ # because these are the only two platforms that can be recognized.
833
+ def detect_platform
834
+ return "dokku" unless ENV.fetch("DOKKU_ROOT", nil).to_s.empty?
835
+ return "heroku" unless ENV.fetch("DYNO", nil).to_s.empty?
836
+
837
+ nil
838
+ end
839
+
840
+ # Detect the hostname the way the agent does: the Heroku dyno name first,
841
+ # then the name the host reports for itself. The agent only detects it for
842
+ # the data it reports itself, so the gem has to do it for collector mode.
843
+ def detect_hostname
844
+ dyno = ENV.fetch("DYNO", nil)
845
+ return dyno unless dyno.to_s.empty?
846
+
847
+ hostname = Socket.gethostname
848
+ hostname unless hostname.to_s.empty?
849
+ rescue SystemCallError => e
850
+ logger.debug "Unable to detect the hostname: #{e.class}: #{e.message}"
851
+ nil
852
+ end
853
+
854
+ def detect_revision_from_platform
855
+ PLATFORM_REVISION_ENV_VARS.each do |env_var|
856
+ revision = ENV.fetch(env_var, nil)
857
+ next if revision.to_s.empty?
858
+
859
+ logger.debug "Detected revision from the #{env_var} environment variable"
860
+ return revision
861
+ end
862
+
863
+ nil
864
+ end
865
+
581
866
  def detect_revision_from_file
582
867
  return unless root_path
583
868
 
@@ -715,9 +1000,68 @@ module Appsignal
715
1000
  config[:sidekiq_report_errors] = "all"
716
1001
  end
717
1002
 
1003
+ # A config file holding a YAML null gives an array option a `nil`, which
1004
+ # nothing downstream expects. Correct it to the empty list the option means.
1005
+ ARRAY_OPTIONS.each_key do |option|
1006
+ config[option] = [] if config_hash[option].nil?
1007
+ end
1008
+
718
1009
  config
719
1010
  end
720
1011
 
1012
+ def determine_derived
1013
+ derived = {}
1014
+
1015
+ DEPRECATED_COLLECTOR_OPTIONS.each do |option, replacements|
1016
+ next unless configured?(option)
1017
+
1018
+ replacements.each do |replacement, derivation|
1019
+ next if set_above_derived?(replacement)
1020
+
1021
+ derived[replacement] = send(derivation, config_hash[option])
1022
+ end
1023
+ end
1024
+
1025
+ derived
1026
+ end
1027
+
1028
+ def derived_as_is(value)
1029
+ value
1030
+ end
1031
+
1032
+ def derived_header_names(value)
1033
+ Array(value).filter_map do |key|
1034
+ Appsignal::Utils::RequestHeaders.header_name(key)
1035
+ end
1036
+ end
1037
+
1038
+ def derived_environment_keys(value)
1039
+ Array(value).reject do |key|
1040
+ Appsignal::Utils::RequestHeaders.header_name(key) ||
1041
+ Appsignal::Utils::RequestHeaders::TRANSLATED_ENV_KEYS.include?(key)
1042
+ end
1043
+ end
1044
+
1045
+ def configured?(option)
1046
+ config_hash[option] != DEFAULT_CONFIG[option] && user_set?(option)
1047
+ end
1048
+
1049
+ def user_set?(option)
1050
+ set_by_any?(APPLICATION_SOURCES, option)
1051
+ end
1052
+
1053
+ def set_above_derived?(option)
1054
+ set_by_any?(sources_above_derived, option)
1055
+ end
1056
+
1057
+ def sources_above_derived
1058
+ SOURCE_ORDER[(SOURCE_ORDER.index(:derived) + 1)..] - [:override]
1059
+ end
1060
+
1061
+ def set_by_any?(sources, option)
1062
+ sources.any? { |source| config_sources.fetch(source).key?(option) }
1063
+ end
1064
+
721
1065
  def merge(new_config)
722
1066
  new_config.each do |key, value|
723
1067
  logger.debug("Config key '#{key}' is being overwritten") unless config_hash[key].nil?
@@ -742,14 +1086,19 @@ module Appsignal
742
1086
  # @see AppSignal Ruby gem configuration
743
1087
  # https://docs.appsignal.com/ruby/configuration.html
744
1088
  class ConfigDSL
745
- # @!visibility private
746
- # @return [Hash] Hash containing the DSL option values
747
- attr_reader :dsl_options
748
-
749
1089
  # @!visibility private
750
1090
  def initialize(config)
751
1091
  @config = config
752
1092
  @dsl_options = {}
1093
+ @assigned_options = Set.new
1094
+ end
1095
+
1096
+ # @!visibility private
1097
+ # @return [Hash] Hash containing the DSL option values
1098
+ def dsl_options
1099
+ @dsl_options.reject do |key, value|
1100
+ !@assigned_options.include?(key) && unchanged_option?(key, value)
1101
+ end
753
1102
  end
754
1103
 
755
1104
  # Returns the application's root path.
@@ -935,6 +1284,12 @@ module Appsignal
935
1284
  # @return [Array<String>] Ignore log messages by substrings
936
1285
  # @!attribute [rw] ignore_namespaces
937
1286
  # @return [Array<String>] Ignore traces by namespaces
1287
+ # @!attribute [rw] keep_request_environment
1288
+ # @return [Array<String>] Rack environment keys to report in collector
1289
+ # mode, named the way Rack names them
1290
+ # @!attribute [rw] keep_request_headers
1291
+ # @return [Array<String>] HTTP request headers to report in collector
1292
+ # mode, named the way OpenTelemetry names them
938
1293
  # @!attribute [rw] request_headers
939
1294
  # @return [Array<String>] HTTP request headers to include in error reports
940
1295
 
@@ -989,14 +1344,23 @@ module Appsignal
989
1344
  if @dsl_options.key?(key)
990
1345
  @dsl_options[key]
991
1346
  else
992
- @dsl_options[key] = @config[key].dup
1347
+ @dsl_options[key] = initial_option_value(key)
993
1348
  end
994
1349
  end
995
1350
 
996
1351
  def update_option(key, value)
1352
+ @assigned_options << key
997
1353
  @dsl_options[key] = value
998
1354
  end
999
1355
 
1356
+ def initial_option_value(key)
1357
+ @config[key].dup
1358
+ end
1359
+
1360
+ def unchanged_option?(key, value)
1361
+ value == initial_option_value(key)
1362
+ end
1363
+
1000
1364
  # Parse tags from various input formats and validate values
1001
1365
  # @param value [Hash, nil] input tags
1002
1366
  # @return [Hash] validated tags with string keys
@@ -70,28 +70,30 @@ 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
  )
77
77
  end
78
78
 
79
79
  def add_headers_to(transaction)
80
- transaction.add_headers(
80
+ transaction.add_request_headers(
81
+ "accept" => "text/html,application/xhtml+xml",
82
+ "accept-encoding" => "gzip, deflate, sdch",
83
+ "accept-language" => "en-US,en;q=0.8,nl;q=0.6",
84
+ "cache-control" => "max-age=0",
85
+ "connection" => "keep-alive",
86
+ "user-agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0)",
87
+ "referer" => "http://appsignal.com/accounts"
88
+ )
89
+ transaction.add_request_environment(
81
90
  "REMOTE_ADDR" => "127.0.0.1",
82
91
  "REQUEST_METHOD" => "GET",
83
92
  "SERVER_NAME" => "localhost",
84
93
  "SERVER_PORT" => "80",
85
94
  "SERVER_PROTOCOL" => "HTTP/1.1",
86
95
  "REQUEST_PATH" => "/hello",
87
- "PATH_INFO" => "/hello",
88
- "HTTP_ACCEPT" => "text/html,application/xhtml+xml",
89
- "HTTP_ACCEPT_ENCODING" => "gzip, deflate, sdch",
90
- "HTTP_ACCEPT_LANGUAGE" => "en-US,en;q=0.8,nl;q=0.6",
91
- "HTTP_CACHE_CONTROL" => "max-age=0",
92
- "HTTP_CONNECTION" => "keep-alive",
93
- "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0)",
94
- "HTTP_REFERER" => "http://appsignal.com/accounts"
96
+ "PATH_INFO" => "/hello"
95
97
  )
96
98
  end
97
99
  end
@@ -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
+ )