appsignal 4.10.4 → 5.0.1

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 +122 -0
  3. data/Rakefile +195 -4
  4. data/appsignal.gemspec +8 -0
  5. data/build_matrix.yml +12 -0
  6. data/ext/agent.rb +27 -27
  7. data/ext/appsignal_extension.c +14 -0
  8. data/lib/appsignal/backends.rb +55 -0
  9. data/lib/appsignal/cli/diagnose.rb +2 -10
  10. data/lib/appsignal/config.rb +377 -13
  11. data/lib/appsignal/demo.rb +12 -10
  12. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
  13. data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
  14. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
  15. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
  16. data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
  17. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
  18. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
  19. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
  20. data/lib/appsignal/event_formatter.rb +78 -0
  21. data/lib/appsignal/extension.rb +4 -0
  22. data/lib/appsignal/helpers/instrumentation.rb +324 -20
  23. data/lib/appsignal/helpers/metrics.rb +3 -24
  24. data/lib/appsignal/hooks/action_cable.rb +26 -8
  25. data/lib/appsignal/hooks/active_job.rb +184 -47
  26. data/lib/appsignal/hooks/at_exit.rb +4 -1
  27. data/lib/appsignal/hooks/excon.rb +20 -0
  28. data/lib/appsignal/hooks/faraday.rb +16 -0
  29. data/lib/appsignal/hooks/http.rb +5 -0
  30. data/lib/appsignal/hooks/resque.rb +1 -1
  31. data/lib/appsignal/hooks/sequel.rb +32 -2
  32. data/lib/appsignal/hooks/shoryuken.rb +3 -3
  33. data/lib/appsignal/hooks/sidekiq.rb +1 -1
  34. data/lib/appsignal/integrations/action_cable.rb +5 -2
  35. data/lib/appsignal/integrations/active_support_notifications.rb +59 -19
  36. data/lib/appsignal/integrations/data_mapper.rb +14 -2
  37. data/lib/appsignal/integrations/delayed_job_plugin.rb +81 -8
  38. data/lib/appsignal/integrations/dry_monitor.rb +39 -15
  39. data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
  40. data/lib/appsignal/integrations/excon.rb +52 -15
  41. data/lib/appsignal/integrations/faraday.rb +47 -12
  42. data/lib/appsignal/integrations/http.rb +43 -1
  43. data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
  44. data/lib/appsignal/integrations/net_http.rb +31 -2
  45. data/lib/appsignal/integrations/puma.rb +4 -1
  46. data/lib/appsignal/integrations/que.rb +256 -37
  47. data/lib/appsignal/integrations/railtie.rb +4 -1
  48. data/lib/appsignal/integrations/rake.rb +9 -3
  49. data/lib/appsignal/integrations/redis.rb +22 -1
  50. data/lib/appsignal/integrations/redis_client.rb +22 -1
  51. data/lib/appsignal/integrations/resque.rb +81 -11
  52. data/lib/appsignal/integrations/shoryuken.rb +159 -12
  53. data/lib/appsignal/integrations/sidekiq.rb +94 -16
  54. data/lib/appsignal/integrations/webmachine.rb +56 -5
  55. data/lib/appsignal/loaders/padrino.rb +2 -1
  56. data/lib/appsignal/logger/extension_backend.rb +24 -0
  57. data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
  58. data/lib/appsignal/logger.rb +13 -9
  59. data/lib/appsignal/metrics/extension_backend.rb +47 -0
  60. data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
  61. data/lib/appsignal/opentelemetry/attributes.rb +31 -0
  62. data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
  63. data/lib/appsignal/opentelemetry/error_type.rb +37 -0
  64. data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
  65. data/lib/appsignal/opentelemetry/http_method.rb +59 -0
  66. data/lib/appsignal/opentelemetry/http_response.rb +30 -0
  67. data/lib/appsignal/opentelemetry/http_server_request.rb +79 -0
  68. data/lib/appsignal/opentelemetry/messaging.rb +82 -0
  69. data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
  70. data/lib/appsignal/opentelemetry/rendering.rb +29 -0
  71. data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
  72. data/lib/appsignal/opentelemetry.rb +494 -0
  73. data/lib/appsignal/rack/abstract_middleware.rb +66 -4
  74. data/lib/appsignal/rack/body_wrapper.rb +18 -5
  75. data/lib/appsignal/rack/event_handler.rb +52 -8
  76. data/lib/appsignal/rack/grape_middleware.rb +1 -0
  77. data/lib/appsignal/rack/hanami_middleware.rb +2 -1
  78. data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
  79. data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
  80. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
  81. data/lib/appsignal/rack.rb +68 -12
  82. data/lib/appsignal/sample_data.rb +4 -0
  83. data/lib/appsignal/transaction/base_backend.rb +128 -0
  84. data/lib/appsignal/transaction/extension_backend.rb +229 -0
  85. data/lib/appsignal/transaction/opentelemetry_backend.rb +851 -0
  86. data/lib/appsignal/transaction.rb +714 -164
  87. data/lib/appsignal/utils/request_headers.rb +78 -0
  88. data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
  89. data/lib/appsignal/utils.rb +1 -0
  90. data/lib/appsignal/version.rb +1 -1
  91. data/lib/appsignal.rb +10 -0
  92. data/sig/appsignal.rbi +630 -37
  93. data/sig/appsignal.rbs +582 -27
  94. metadata +25 -1
@@ -834,10 +834,23 @@ static VALUE add_distribution_value(VALUE self, VALUE key, VALUE value, VALUE ta
834
834
  return Qnil;
835
835
  }
836
836
 
837
+ // Per-thread running total of object allocations, incremented on every Ruby
838
+ // NEWOBJ event. Thread-local because MRI maps each Ruby thread to its own OS
839
+ // thread, so this attributes allocations to the thread doing the work, which is
840
+ // the thread the transaction runs on. Collector mode reads it through
841
+ // Appsignal::Extension.allocation_count and diffs two snapshots to get the
842
+ // allocations made during a transaction or an event.
843
+ static __thread unsigned long long appsignal_thread_allocation_count = 0;
844
+
837
845
  static void track_allocation(rb_event_flag_t flag, VALUE arg1, VALUE arg2, ID arg3, VALUE arg4) {
846
+ appsignal_thread_allocation_count++;
838
847
  appsignal_track_allocation();
839
848
  }
840
849
 
850
+ static VALUE allocation_count(VALUE self) {
851
+ return ULL2NUM(appsignal_thread_allocation_count);
852
+ }
853
+
841
854
  static VALUE install_allocation_event_hook(VALUE self) {
842
855
  // This event hook is only available on Ruby 2.1 and 2.2
843
856
  #if defined(RUBY_INTERNAL_EVENT_NEWOBJ)
@@ -956,6 +969,7 @@ void Init_appsignal_extension(void) {
956
969
 
957
970
  // Other helper methods
958
971
  rb_define_singleton_method(Extension, "install_allocation_event_hook", install_allocation_event_hook, 0);
972
+ rb_define_singleton_method(Extension, "allocation_count", allocation_count, 0);
959
973
  rb_define_singleton_method(Extension, "running_in_container?", running_in_container, 0);
960
974
  rb_define_singleton_method(Extension, "set_environment_metadata", set_environment_metadata, 2);
961
975
 
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal/metrics/extension_backend"
4
+ require "appsignal/metrics/opentelemetry_backend"
5
+ require "appsignal/logger/extension_backend"
6
+ require "appsignal/logger/opentelemetry_backend"
7
+ require "appsignal/transaction/base_backend"
8
+ require "appsignal/transaction/extension_backend"
9
+ require "appsignal/transaction/opentelemetry_backend"
10
+
11
+ module Appsignal
12
+ # @!visibility private
13
+ #
14
+ # Looks up the active backend for each AppSignal subsystem. In normal
15
+ # operation, subsystems route through the C-extension (and its agent).
16
+ # When collector mode is configured and the OpenTelemetry SDK has booted
17
+ # successfully, supported subsystems route through OTel instead.
18
+ #
19
+ # Centralizes the mode-check so per-subsystem call sites don't repeat the
20
+ # "if collector? then OTel else Extension" branch. Future subsystems plug
21
+ # in by adding one more lookup method here.
22
+ module Backends
23
+ class << self
24
+ def metrics
25
+ if collector?
26
+ Appsignal::Metrics::OpenTelemetryBackend
27
+ else
28
+ Appsignal::Metrics::ExtensionBackend
29
+ end
30
+ end
31
+
32
+ def logger
33
+ if collector?
34
+ Appsignal::Logger::OpenTelemetryBackend
35
+ else
36
+ Appsignal::Logger::ExtensionBackend
37
+ end
38
+ end
39
+
40
+ def transaction
41
+ if collector?
42
+ Appsignal::Transaction::OpenTelemetryBackend
43
+ else
44
+ Appsignal::Transaction::ExtensionBackend
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def collector?
51
+ Appsignal.config&.collector_mode? || false
52
+ end
53
+ end
54
+ end
55
+ end
@@ -201,6 +201,7 @@ module Appsignal
201
201
 
202
202
  # No config loaded yet, try loading as normal
203
203
  Appsignal._load_config!(env_option) unless Appsignal.config
204
+ Appsignal.config.apply_overrides
204
205
  Appsignal._start_logger
205
206
  Appsignal.config.write_to_environment
206
207
  Appsignal.internal_logger.info("Starting AppSignal diagnose")
@@ -472,16 +473,7 @@ module Appsignal
472
473
  config = Appsignal.config
473
474
  data[:config] = {
474
475
  :options => config.config_hash.merge(:env => config.env),
475
- :sources => {
476
- :default => Appsignal::Config::DEFAULT_CONFIG,
477
- :system => config.system_config,
478
- :loaders => config.loaders_config,
479
- :initial => config.initial_config,
480
- :file => config.file_config,
481
- :env => config.env_config,
482
- :override => config.override_config,
483
- :dsl => config.dsl_config
484
- }
476
+ :sources => config.config_sources
485
477
  }
486
478
  print_config_options(config)
487
479
  end
@@ -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 => "app",
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