appsignal 4.10.1-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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Rakefile +87 -0
  4. data/appsignal.gemspec +8 -0
  5. data/build_matrix.yml +14 -1
  6. data/ext/appsignal_extension.c +14 -0
  7. data/lib/appsignal/backends.rb +55 -0
  8. data/lib/appsignal/config.rb +140 -1
  9. data/lib/appsignal/demo.rb +1 -1
  10. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
  11. data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
  12. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
  13. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
  14. data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
  15. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
  16. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
  17. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
  18. data/lib/appsignal/event_formatter.rb +78 -0
  19. data/lib/appsignal/extension.rb +4 -0
  20. data/lib/appsignal/helpers/instrumentation.rb +236 -20
  21. data/lib/appsignal/helpers/metrics.rb +3 -24
  22. data/lib/appsignal/hooks/action_cable.rb +18 -6
  23. data/lib/appsignal/hooks/active_job.rb +128 -39
  24. data/lib/appsignal/hooks/at_exit.rb +4 -1
  25. data/lib/appsignal/hooks/excon.rb +20 -0
  26. data/lib/appsignal/hooks/faraday.rb +16 -0
  27. data/lib/appsignal/hooks/http.rb +5 -0
  28. data/lib/appsignal/hooks/resque.rb +1 -1
  29. data/lib/appsignal/hooks/sequel.rb +32 -2
  30. data/lib/appsignal/hooks/shoryuken.rb +3 -3
  31. data/lib/appsignal/hooks/sidekiq.rb +1 -1
  32. data/lib/appsignal/integrations/action_cable.rb +5 -2
  33. data/lib/appsignal/integrations/active_support_notifications.rb +59 -14
  34. data/lib/appsignal/integrations/data_mapper.rb +14 -2
  35. data/lib/appsignal/integrations/delayed_job_plugin.rb +45 -5
  36. data/lib/appsignal/integrations/dry_monitor.rb +39 -15
  37. data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
  38. data/lib/appsignal/integrations/excon.rb +52 -15
  39. data/lib/appsignal/integrations/faraday.rb +47 -12
  40. data/lib/appsignal/integrations/http.rb +43 -1
  41. data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
  42. data/lib/appsignal/integrations/net_http.rb +31 -2
  43. data/lib/appsignal/integrations/puma.rb +4 -1
  44. data/lib/appsignal/integrations/que.rb +224 -37
  45. data/lib/appsignal/integrations/railtie.rb +4 -1
  46. data/lib/appsignal/integrations/rake.rb +9 -3
  47. data/lib/appsignal/integrations/redis.rb +22 -1
  48. data/lib/appsignal/integrations/redis_client.rb +22 -1
  49. data/lib/appsignal/integrations/resque.rb +57 -10
  50. data/lib/appsignal/integrations/shoryuken.rb +140 -12
  51. data/lib/appsignal/integrations/sidekiq.rb +73 -16
  52. data/lib/appsignal/integrations/webmachine.rb +47 -4
  53. data/lib/appsignal/loaders/padrino.rb +2 -1
  54. data/lib/appsignal/logger/extension_backend.rb +24 -0
  55. data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
  56. data/lib/appsignal/logger.rb +13 -9
  57. data/lib/appsignal/metrics/extension_backend.rb +47 -0
  58. data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
  59. data/lib/appsignal/opentelemetry/attributes.rb +31 -0
  60. data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
  61. data/lib/appsignal/opentelemetry/error_type.rb +37 -0
  62. data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
  63. data/lib/appsignal/opentelemetry/http_method.rb +59 -0
  64. data/lib/appsignal/opentelemetry/http_response.rb +30 -0
  65. data/lib/appsignal/opentelemetry/http_server_request.rb +45 -0
  66. data/lib/appsignal/opentelemetry/messaging.rb +82 -0
  67. data/lib/appsignal/opentelemetry/rendering.rb +29 -0
  68. data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
  69. data/lib/appsignal/opentelemetry.rb +337 -0
  70. data/lib/appsignal/rack/abstract_middleware.rb +63 -4
  71. data/lib/appsignal/rack/body_wrapper.rb +18 -5
  72. data/lib/appsignal/rack/event_handler.rb +36 -3
  73. data/lib/appsignal/rack/grape_middleware.rb +1 -0
  74. data/lib/appsignal/rack/hanami_middleware.rb +2 -1
  75. data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
  76. data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
  77. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
  78. data/lib/appsignal/rack.rb +39 -11
  79. data/lib/appsignal/sample_data.rb +4 -0
  80. data/lib/appsignal/transaction/base_backend.rb +107 -0
  81. data/lib/appsignal/transaction/extension_backend.rb +203 -0
  82. data/lib/appsignal/transaction/opentelemetry_backend.rb +796 -0
  83. data/lib/appsignal/transaction.rb +537 -144
  84. data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
  85. data/lib/appsignal/version.rb +1 -1
  86. data/lib/appsignal.rb +9 -0
  87. data/sig/appsignal.rbi +426 -37
  88. data/sig/appsignal.rbs +386 -27
  89. metadata +23 -1
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal/opentelemetry/attributes"
4
+ require "appsignal/opentelemetry/dependencies"
5
+ require "appsignal/opentelemetry/error_type"
6
+ require "appsignal/opentelemetry/http_client_request"
7
+ require "appsignal/opentelemetry/http_method"
8
+ require "appsignal/opentelemetry/http_response"
9
+ require "appsignal/opentelemetry/http_server_request"
10
+ require "appsignal/opentelemetry/messaging"
11
+ require "appsignal/opentelemetry/rendering"
12
+ require "appsignal/opentelemetry/sql_db_system"
13
+
14
+ module Appsignal
15
+ # @!visibility private
16
+ module OpenTelemetry
17
+ class << self
18
+ # Configure the global OpenTelemetry SDK to export OTLP/HTTP protobuf to
19
+ # the collector endpoint in `config[:collector_endpoint]`.
20
+ #
21
+ # The SDK and exporter gems are required lazily, so an application not in
22
+ # collector mode does not pay the load cost. Sets `@started`, which
23
+ # {.started?} reads to decide whether to route through the OTel backends.
24
+ def configure(config)
25
+ # The OTel Ruby SDK exposes no programmatic knob for the default
26
+ # aggregation temporality; this env var is the only way to set
27
+ # it. We pick `:delta` to match the Python integration. (Note:
28
+ # the Ruby SDK keeps `UpDownCounter` cumulative regardless of
29
+ # this preference, per the OTel spec.)
30
+ ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] ||= "delta"
31
+
32
+ # With the metrics and logs SDK gems loaded, `SDK.configure` below
33
+ # auto-installs a metrics reader and a log processor from these env vars,
34
+ # each with its own background thread. Both providers are replaced right
35
+ # after, which would leave those threads running and unreachable by any
36
+ # shutdown. Set unconditionally, so a user-set "otlp" cannot slip past
37
+ # and reintroduce them.
38
+ ENV["OTEL_METRICS_EXPORTER"] = "none"
39
+ ENV["OTEL_LOGS_EXPORTER"] = "none"
40
+
41
+ require_sdk_gems
42
+
43
+ # The OpenTelemetry gems are optional and installed by the user (not
44
+ # declared in the gemspec). If they're present but older than the
45
+ # versions we support, fall back to the agent rather than booting an
46
+ # SDK that may misbehave (e.g. a metrics SDK without fork hooks).
47
+ return unless required_gem_versions_met?
48
+
49
+ endpoint = config[:collector_endpoint].to_s.sub(%r{/+\z}, "")
50
+ # Merge with the SDK's default resource so all three signal types
51
+ # carry the same `telemetry.sdk.*` and `process.*` attributes that
52
+ # `SDK.configure` would have added on its own. `MeterProvider` and
53
+ # `LoggerProvider` take a `resource:` kwarg that replaces (not
54
+ # merges), so we do the merge ourselves and use the same merged
55
+ # resource for the tracer provider to keep all three in sync.
56
+ resource = ::OpenTelemetry::SDK::Resources::Resource.default.merge(build_resource(config))
57
+
58
+ ::OpenTelemetry::SDK.configure do |c|
59
+ c.resource = resource
60
+ c.add_span_processor(
61
+ ::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
62
+ ::OpenTelemetry::Exporter::OTLP::Exporter.new(
63
+ :endpoint => "#{endpoint}/v1/traces"
64
+ )
65
+ )
66
+ )
67
+ end
68
+
69
+ # Wrap the OTLP MetricsExporter in a PeriodicMetricReader so that
70
+ # `MeterProvider#force_flush` actually triggers an export. The OTLP
71
+ # exporter itself is also a MetricReader but its inherited
72
+ # `force_flush` is a no-op.
73
+ ::OpenTelemetry.meter_provider =
74
+ ::OpenTelemetry::SDK::Metrics::MeterProvider.new(:resource => resource)
75
+ ::OpenTelemetry.meter_provider.add_metric_reader(
76
+ ::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
77
+ :exporter => ::OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(
78
+ :endpoint => "#{endpoint}/v1/metrics"
79
+ )
80
+ )
81
+ )
82
+
83
+ ::OpenTelemetry.logger_provider =
84
+ ::OpenTelemetry::SDK::Logs::LoggerProvider.new(:resource => resource)
85
+ ::OpenTelemetry.logger_provider.add_log_record_processor(
86
+ ::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(
87
+ ::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
88
+ :endpoint => "#{endpoint}/v1/logs"
89
+ )
90
+ )
91
+ )
92
+
93
+ @started = true
94
+ rescue LoadError => e
95
+ @started = false
96
+ Appsignal::Utils::StdoutAndLoggerMessage.error(
97
+ "Cannot configure OpenTelemetry SDK for collector mode: #{e.class}: #{e.message}"
98
+ )
99
+ rescue => e
100
+ @started = false
101
+ Appsignal::Utils::StdoutAndLoggerMessage.error(
102
+ "Error configuring OpenTelemetry SDK for collector mode: " \
103
+ "#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}"
104
+ )
105
+ end
106
+
107
+ # Whether {.configure} has successfully booted the OpenTelemetry SDK
108
+ # for this process. Returns `false` before {.configure} runs and
109
+ # `false` if it ran but raised.
110
+ def started?
111
+ defined?(@started) ? @started : false
112
+ end
113
+
114
+ # Write the current trace context onto an outgoing carrier (HTTP request,
115
+ # job hash, ...) with the configured propagator, so a downstream service
116
+ # joins the same trace.
117
+ #
118
+ # A no-op unless the SDK has booted. The carrier is injected from whatever
119
+ # span is current at call time, which inside an `Appsignal.instrument`
120
+ # block is the event's own span.
121
+ def inject_context(carrier)
122
+ if_started do
123
+ ::OpenTelemetry.propagation.inject(carrier)
124
+ end
125
+ end
126
+
127
+ # Read the trace context off an incoming Rack request env using the
128
+ # globally configured propagator, so an AppSignal transaction created for
129
+ # the request can continue the upstream trace. Returns an
130
+ # `OpenTelemetry::Context` (its current span is the remote parent), or
131
+ # `nil` when the SDK has not booted -- outside collector mode there is
132
+ # nothing to continue. `rack_env_getter` reads the `HTTP_*`-mangled header
133
+ # names Rack puts in the env.
134
+ def extract_rack_context(env)
135
+ if_started do
136
+ # Extract onto an empty context, not `Context.current`. The W3C
137
+ # extractor returns its base context unchanged when the carrier has no
138
+ # `traceparent`, so a request with no incoming context would inherit
139
+ # whatever span is ambient on the fiber. Starting empty means "no
140
+ # carrier" yields no parent, and the transaction starts its own trace.
141
+ ::OpenTelemetry.propagation.extract(
142
+ env,
143
+ :context => ::OpenTelemetry::Context.empty,
144
+ :getter => ::OpenTelemetry::Common::Propagation.rack_env_getter
145
+ )
146
+ end
147
+ end
148
+
149
+ # Read the trace context off an incoming background job hash, so the
150
+ # transaction can link back to the enqueuer. Returns an
151
+ # `OpenTelemetry::Context`, or `nil` when the SDK has not booted.
152
+ #
153
+ # Reads both carriers a job can arrive with: top-level `traceparent` and
154
+ # `tracestate` keys, as OpenTelemetry's Sidekiq instrumentation injects
155
+ # them, and a nested `__otel_headers`, as its Active Job one does. Active
156
+ # Job puts that through its argument serializer, so it can arrive as an
157
+ # array of pairs rather than a hash. The nested keys win, being the more
158
+ # specific layer.
159
+ def extract_job_context(item)
160
+ if_started do
161
+ carrier = item
162
+ nested = item["__otel_headers"]
163
+ nested = nested.to_h if otel_header_pairs?(nested)
164
+ carrier = item.merge(nested) if nested.is_a?(Hash)
165
+ # Extract onto an empty context rather than the default
166
+ # `Context.current`, for the same reason as `extract_rack_context`:
167
+ # a job with no injected trace context must not inherit an ambient
168
+ # span left on the fiber. Otherwise the job's transaction would link
169
+ # back to an unrelated leaked span instead of standing on its own.
170
+ ::OpenTelemetry.propagation.extract(
171
+ carrier,
172
+ :context => ::OpenTelemetry::Context.empty
173
+ )
174
+ end
175
+ end
176
+
177
+ # Run `block` only when the OpenTelemetry SDK has booted (collector mode),
178
+ # returning its result; a no-op returning `nil` otherwise. The block can
179
+ # touch the OTel SDK freely -- it only runs when the SDK is loaded.
180
+ #
181
+ # This is the gate every integration's OTel-specific work goes through, so
182
+ # integration-specific carrier/getter/setter logic lives in the
183
+ # integration rather than as a bespoke helper here.
184
+ def if_started
185
+ return unless started?
186
+
187
+ yield
188
+ end
189
+
190
+ # @!visibility private
191
+ #
192
+ # Test-only. Drops the started flag so subsequent tests start from a
193
+ # clean slate; does not touch the global `::OpenTelemetry` providers.
194
+ def reset!
195
+ @started = false
196
+ end
197
+
198
+ # Flush and shut down the OpenTelemetry SDK providers booted by
199
+ # {.configure}. Called from `Appsignal.stop` so buffered
200
+ # metrics/logs/spans don't get dropped on exit.
201
+ def shutdown
202
+ return unless started?
203
+
204
+ ::OpenTelemetry.tracer_provider&.shutdown
205
+ ::OpenTelemetry.meter_provider&.shutdown
206
+ ::OpenTelemetry.logger_provider&.shutdown
207
+ rescue => e
208
+ Appsignal.internal_logger.error(
209
+ "Error shutting down OpenTelemetry SDK: #{e.class}: #{e.message}"
210
+ )
211
+ end
212
+
213
+ # Build the OpenTelemetry Resource that carries AppSignal config to the
214
+ # collector. Attributes whose underlying option is nil or an empty array
215
+ # are omitted so the collector applies its own defaults.
216
+ def build_resource(config)
217
+ revision = config[:revision].to_s.empty? ? "unknown" : config[:revision]
218
+ service_name = config[:service_name].to_s.empty? ? "unknown" : config[:service_name]
219
+ host_name = config[:hostname].to_s.empty? ? "unknown" : config[:hostname]
220
+
221
+ attrs = {
222
+ "appsignal.config.name" => config[:name],
223
+ "appsignal.config.environment" => config.env,
224
+ "appsignal.config.push_api_key" => config[:push_api_key],
225
+ "appsignal.config.revision" => revision,
226
+ "appsignal.config.language_integration" => "ruby",
227
+ "service.name" => service_name,
228
+ "host.name" => host_name,
229
+ "appsignal.config.filter_attributes" => config[:filter_attributes],
230
+ "appsignal.config.filter_function_parameters" => config[:filter_function_parameters],
231
+ "appsignal.config.filter_request_query_parameters" =>
232
+ config[:filter_request_query_parameters],
233
+ "appsignal.config.filter_request_payload" => config[:filter_request_payload],
234
+ "appsignal.config.filter_request_session_data" => config[:filter_session_data],
235
+ "appsignal.config.ignore_actions" => config[:ignore_actions],
236
+ "appsignal.config.ignore_errors" => config[:ignore_errors],
237
+ "appsignal.config.ignore_namespaces" => config[:ignore_namespaces],
238
+ "appsignal.config.response_headers" => config[:response_headers],
239
+ "appsignal.config.request_headers" => config[:request_headers],
240
+ "appsignal.config.send_function_parameters" => config[:send_function_parameters],
241
+ "appsignal.config.send_request_query_parameters" =>
242
+ config[:send_request_query_parameters],
243
+ "appsignal.config.send_request_payload" => config[:send_request_payload],
244
+ "appsignal.config.send_request_session_data" => config[:send_session_data]
245
+ }
246
+ attrs.reject! { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
247
+ ::OpenTelemetry::SDK::Resources::Resource.create(attrs)
248
+ end
249
+
250
+ private
251
+
252
+ # Whether a `__otel_headers` value is the array-of-`[key, value]`-pairs
253
+ # shape produced by ActiveJob's argument serializer, so it can be turned
254
+ # into a hash carrier. Anything else (including a malformed array) is left
255
+ # alone rather than raising on `to_h` inside a job perform.
256
+ def otel_header_pairs?(value)
257
+ value.is_a?(Array) && value.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
258
+ end
259
+
260
+ # The optional OpenTelemetry gems, required lazily so users not in
261
+ # collector mode don't pay the load cost. A missing gem raises LoadError,
262
+ # caught by {.configure}.
263
+ def require_sdk_gems
264
+ require "opentelemetry/sdk"
265
+ require "opentelemetry-common"
266
+ require "opentelemetry/exporter/otlp"
267
+ require "opentelemetry-metrics-sdk"
268
+ require "opentelemetry-exporter-otlp-metrics"
269
+ require "opentelemetry-logs-sdk"
270
+ require "opentelemetry-exporter-otlp-logs"
271
+ end
272
+
273
+ # Checks the installed OpenTelemetry gem versions against {REQUIRED_GEMS}.
274
+ # On a shortfall, warns and flags the SDK as not started so the caller
275
+ # falls back to the agent; returns whether all requirements are met.
276
+ def required_gem_versions_met?
277
+ incompatible = incompatible_gems
278
+ return true if incompatible.nil?
279
+
280
+ @started = false
281
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
282
+ collector_gems_warning(incompatible)
283
+ )
284
+ false
285
+ end
286
+
287
+ # Builds the warning shown when the OpenTelemetry gems collector mode needs
288
+ # are not all installed at a supported version. `incompatible` describes
289
+ # the gems that are installed but at a version we do not support.
290
+ #
291
+ # The message always recommends the `appsignal-opentelemetry` gem, which
292
+ # installs the whole set. It only lists gems when some are installed at an
293
+ # incompatible version, because that usually means a constraint in the
294
+ # bundle that the gem cannot override on its own.
295
+ def collector_gems_warning(incompatible)
296
+ message =
297
+ "AppSignal collector mode requires a set of OpenTelemetry gems. " \
298
+ "Add the `appsignal-opentelemetry` gem to your bundle to install " \
299
+ "them. The AppSignal agent will be used instead."
300
+
301
+ unless incompatible.empty?
302
+ message += "\n\nThese installed OpenTelemetry gems are not compatible " \
303
+ "with this AppSignal version. Update them or remove a version " \
304
+ "constraint:\n"
305
+ message += incompatible.map { |line| "- #{line}" }.join("\n")
306
+ end
307
+
308
+ message
309
+ end
310
+
311
+ # Checks the installed OpenTelemetry gems against {REQUIRED_GEMS}. Returns
312
+ # `nil` when every required gem is installed at a supported version.
313
+ # Otherwise returns the descriptions of gems that are installed but at a
314
+ # version we do not support. That list is empty when the only problem is
315
+ # that some required gems are not installed at all.
316
+ def incompatible_gems
317
+ missing = false
318
+ incompatible = []
319
+
320
+ REQUIRED_GEMS.each do |name, constraints|
321
+ spec = Gem.loaded_specs[name]
322
+ requirement = Gem::Requirement.new(*constraints)
323
+
324
+ if spec.nil?
325
+ missing = true
326
+ elsif !requirement.satisfied_by?(spec.version)
327
+ incompatible << "#{name} #{spec.version} (requires #{requirement})"
328
+ end
329
+ end
330
+
331
+ return nil if !missing && incompatible.empty?
332
+
333
+ incompatible
334
+ end
335
+ end
336
+ end
337
+ end
@@ -20,6 +20,10 @@ module Appsignal
20
20
  @options = options
21
21
  @request_class = options.fetch(:request_class, ::Rack::Request)
22
22
  @instrument_event_name = options.fetch(:instrument_event_name, nil)
23
+ # The OpenTelemetry instrumentation scope for the request's spans in
24
+ # collector mode. Each framework middleware sets its own; nil falls back
25
+ # to the default scope in the backend.
26
+ @opentelemetry_scope = options.fetch(:opentelemetry_scope, nil)
23
27
  @report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
24
28
  end
25
29
 
@@ -33,7 +37,11 @@ module Appsignal
33
37
  if wrapped_instrumentation
34
38
  env[Appsignal::Rack::APPSIGNAL_TRANSACTION]
35
39
  else
36
- Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
40
+ Appsignal::Transaction.create(
41
+ Appsignal::Transaction::HTTP_REQUEST,
42
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(env),
43
+ :opentelemetry_scope => @opentelemetry_scope
44
+ )
37
45
  end
38
46
 
39
47
  unless wrapped_instrumentation
@@ -41,6 +49,7 @@ module Appsignal
41
49
  # middleware can detect if there is parent instrumentation
42
50
  # middleware active.
43
51
  env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = transaction
52
+ add_opentelemetry_request_attributes(transaction, request)
44
53
  end
45
54
 
46
55
  begin
@@ -59,8 +68,13 @@ module Appsignal
59
68
  ensure
60
69
  add_transaction_metadata_after(transaction, request)
61
70
 
62
- # Complete transaction because this is the top instrumentation middleware.
63
- Appsignal::Transaction.complete_current! unless wrapped_instrumentation
71
+ unless wrapped_instrumentation
72
+ add_opentelemetry_response_attributes(transaction, request)
73
+
74
+ # Complete transaction because this is the top instrumentation
75
+ # middleware.
76
+ Appsignal::Transaction.complete_current!
77
+ end
64
78
  end
65
79
  else
66
80
  @app.call(env)
@@ -69,6 +83,45 @@ module Appsignal
69
83
 
70
84
  private
71
85
 
86
+ # Describes the transaction's span as an incoming HTTP request. Together
87
+ # with the SERVER span kind the transaction already carries, this is what
88
+ # the trace timeline reads to recognize a web request.
89
+ #
90
+ # Set where the transaction is created, rather than with the rest of the
91
+ # request metadata in {ApplyRackRequest}, which runs after the app has been
92
+ # called: by then an event span may be open, and the attributes would land
93
+ # on that instead. Only the middleware that created the transaction sets
94
+ # them, so nested middleware does not write them again.
95
+ def add_opentelemetry_request_attributes(transaction, request)
96
+ transaction.add_opentelemetry_attributes(
97
+ Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
98
+ :method => Appsignal::Rack::Utils.request_method_from(request),
99
+ :path => Appsignal::Rack::Utils.request_value_from(request, :path),
100
+ :scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
101
+ :query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
102
+ )
103
+ )
104
+ end
105
+
106
+ # Describes the response the app produced on the transaction's span, which
107
+ # the semantic conventions ask for whenever a response was sent.
108
+ #
109
+ # Set from the `ensure` in {#call} rather than from {#call_app}, where the
110
+ # status is first known. That runs inside the instrumented event, so the
111
+ # attribute would land on the event span instead of on the transaction's
112
+ # own span. The status travels between the two in the request environment
113
+ # for that reason.
114
+ #
115
+ # A request whose app raised never produced a status, and is described
116
+ # without one.
117
+ def add_opentelemetry_response_attributes(transaction, request)
118
+ transaction.add_opentelemetry_attributes(
119
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(
120
+ request.env[Appsignal::Rack::APPSIGNAL_RESPONSE_STATUS]
121
+ )
122
+ )
123
+ end
124
+
72
125
  # Another instrumentation middleware is active earlier in the stack, so
73
126
  # don't report any exceptions here, the top instrumentation middleware
74
127
  # will be the one reporting the exception.
@@ -79,7 +132,10 @@ module Appsignal
79
132
  # @see #instrument_app_call_with_exception_handling
80
133
  def instrument_app_call(env, transaction)
81
134
  if @instrument_event_name
82
- Appsignal.instrument(@instrument_event_name) do
135
+ Appsignal.instrument(
136
+ @instrument_event_name,
137
+ :opentelemetry_scope => @opentelemetry_scope
138
+ ) do
83
139
  call_app(env, transaction)
84
140
  end
85
141
  else
@@ -89,6 +145,9 @@ module Appsignal
89
145
 
90
146
  def call_app(env, transaction)
91
147
  status, headers, obody = @app.call(env)
148
+ # Remember the status for {#add_opentelemetry_response_attributes}, which
149
+ # runs once this event has closed.
150
+ env[Appsignal::Rack::APPSIGNAL_RESPONSE_STATUS] = status
92
151
  body =
93
152
  if env[Appsignal::Rack::APPSIGNAL_RESPONSE_INSTRUMENTED]
94
153
  obody
@@ -48,7 +48,10 @@ module Appsignal
48
48
  # of the body has already closed itself (as prescribed) we do not
49
49
  # attempt to close it twice
50
50
  if !@body_already_closed && @body.respond_to?(:close)
51
- Appsignal.instrument("close_response_body.rack") { @body.close }
51
+ Appsignal.instrument(
52
+ "close_response_body.rack",
53
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
54
+ ) { @body.close }
52
55
  end
53
56
  @body_already_closed = true
54
57
  rescue *IGNORED_ERRORS # Do not report
@@ -104,7 +107,11 @@ module Appsignal
104
107
  # in a blockless way it is still a good idea to have it in place.
105
108
  return enum_for(:each) unless block_given?
106
109
 
107
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#each)") do
110
+ Appsignal.instrument(
111
+ "process_response_body.rack",
112
+ "Process Rack response body (#each)",
113
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
114
+ ) do
108
115
  @body.each(&blk)
109
116
  end
110
117
  rescue *IGNORED_ERRORS # Do not report
@@ -125,7 +132,11 @@ module Appsignal
125
132
  def call(stream)
126
133
  # `stream` will be closed by the app we are calling, no need for us
127
134
  # to close it ourselves
128
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#call)") do
135
+ Appsignal.instrument(
136
+ "process_response_body.rack",
137
+ "Process Rack response body (#call)",
138
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
139
+ ) do
129
140
  @body.call(stream)
130
141
  end
131
142
  rescue *IGNORED_ERRORS # Do not report
@@ -150,7 +161,8 @@ module Appsignal
150
161
  @body_already_closed = true
151
162
  Appsignal.instrument(
152
163
  "process_response_body.rack",
153
- "Process Rack response body (#to_ary)"
164
+ "Process Rack response body (#to_ary)",
165
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
154
166
  ) do
155
167
  @body.to_ary
156
168
  end
@@ -168,7 +180,8 @@ module Appsignal
168
180
  def to_path
169
181
  Appsignal.instrument(
170
182
  "process_response_body.rack",
171
- "Process Rack response body (#to_path)"
183
+ "Process Rack response body (#to_path)",
184
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
172
185
  ) do
173
186
  @body.to_path
174
187
  end
@@ -63,8 +63,30 @@ module Appsignal
63
63
  request.env[APPSIGNAL_EVENT_HANDLER_ID] ||= id
64
64
  return unless request_handler?(request.env[APPSIGNAL_EVENT_HANDLER_ID])
65
65
 
66
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
67
- transaction.start_event
66
+ transaction = Appsignal::Transaction.create(
67
+ Appsignal::Transaction::HTTP_REQUEST,
68
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(request.env),
69
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
70
+ )
71
+ # Describes the transaction's span as an incoming HTTP request.
72
+ # Together with the SERVER span kind the transaction already carries,
73
+ # this is what the trace timeline reads to recognize a web request.
74
+ #
75
+ # Set before the event below starts, because attributes go on
76
+ # whichever span is open and these belong on the transaction's own
77
+ # span. That event stays open until the response finishes, so there is
78
+ # no later point in the request at which these could be set.
79
+ transaction.add_opentelemetry_attributes(
80
+ Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
81
+ :method => Appsignal::Rack::Utils.request_method_from(request),
82
+ :path => Appsignal::Rack::Utils.request_value_from(request, :path),
83
+ :scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
84
+ :query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
85
+ )
86
+ )
87
+ transaction.start_event(
88
+ :opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
89
+ )
68
90
  request.env[APPSIGNAL_TRANSACTION] = transaction
69
91
 
70
92
  request.env[RACK_AFTER_REPLY] ||= []
@@ -118,13 +140,24 @@ module Appsignal
118
140
 
119
141
  self.class.safe_execution("Appsignal::Rack::EventHandler#on_finish") do
120
142
  transaction.finish_event("process_request.rack", "callback: on_finish", "")
121
- transaction.add_params_if_nil { request.params }
143
+ transaction.add_request_payload_if_nil { request.params }
122
144
  transaction.add_headers_if_nil { request.env }
123
145
  transaction.add_session_data_if_nil do
124
146
  request.session if request.respond_to?(:session)
125
147
  end
126
148
  queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
127
149
  transaction.set_queue_start(queue_start) if queue_start
150
+ # Describes the response on the transaction's span, which the semantic
151
+ # conventions ask for whenever a response was sent. It can be set here
152
+ # because the `process_request.rack` event was finished above, which
153
+ # leaves the transaction's own span as the one attributes go on.
154
+ #
155
+ # Only a response the app actually produced counts. The 500 below
156
+ # stands in for a status that was never sent, so it is reported as a
157
+ # tag and a metric but not as this attribute.
158
+ transaction.add_opentelemetry_attributes(
159
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(response&.status)
160
+ )
128
161
  response_status =
129
162
  if response
130
163
  response.status
@@ -7,6 +7,7 @@ module Appsignal
7
7
  # @api private
8
8
  def initialize(app, options = {})
9
9
  options[:instrument_event_name] = "process_request.grape"
10
+ options[:opentelemetry_scope] = ["appsignal-ruby/grape", Appsignal::VERSION]
10
11
  options[:report_errors] = lambda { |env| !env["grape.skip_appsignal_error"] }
11
12
  super
12
13
  end
@@ -7,6 +7,7 @@ module Appsignal
7
7
  def initialize(app, options = {})
8
8
  options[:params_method] = nil
9
9
  options[:instrument_event_name] ||= "process_action.hanami"
10
+ options[:opentelemetry_scope] ||= ["appsignal-ruby/hanami", Appsignal::VERSION]
10
11
  super
11
12
  end
12
13
 
@@ -19,7 +20,7 @@ module Appsignal
19
20
  def add_transaction_metadata_after(transaction, request)
20
21
  action_name = fetch_hanami_action(request.env)
21
22
  transaction.set_action_if_nil(action_name) if action_name
22
- transaction.add_params { params_for(request) }
23
+ transaction.add_request_payload { params_for(request) }
23
24
  end
24
25
 
25
26
  def params_for(request)
@@ -55,6 +55,7 @@ module Appsignal
55
55
  class InstrumentationMiddleware < AbstractMiddleware
56
56
  def initialize(app, options = {})
57
57
  options[:instrument_event_name] ||= "process_request_middleware.rack"
58
+ options[:opentelemetry_scope] ||= ["appsignal-ruby/rack", Appsignal::VERSION]
58
59
  super
59
60
  end
60
61
  end
@@ -8,6 +8,7 @@ module Appsignal
8
8
  options[:request_class] ||= ActionDispatch::Request
9
9
  options[:params_method] ||= :filtered_parameters
10
10
  options[:instrument_event_name] = nil
11
+ options[:opentelemetry_scope] = ["appsignal-ruby/rails", Appsignal::VERSION]
11
12
  options[:report_errors] = true
12
13
  super
13
14
  end
@@ -33,6 +33,7 @@ module Appsignal
33
33
  options[:request_class] ||= Sinatra::Request
34
34
  options[:params_method] ||= :params
35
35
  options[:instrument_event_name] ||= "process_action.sinatra"
36
+ options[:opentelemetry_scope] ||= ["appsignal-ruby/sinatra", Appsignal::VERSION]
36
37
  super
37
38
  @raise_errors_on = raise_errors?(app)
38
39
  end