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
@@ -7,9 +7,46 @@ module Appsignal
7
7
  APPSIGNAL_EVENT_HANDLER_ID = "appsignal.event_handler_id"
8
8
  APPSIGNAL_EVENT_HANDLER_HAS_ERROR = "appsignal.event_handler.error"
9
9
  APPSIGNAL_RESPONSE_INSTRUMENTED = "appsignal.response_instrumentation_active"
10
+ APPSIGNAL_RESPONSE_STATUS = "appsignal.response_status"
10
11
  RACK_AFTER_REPLY = "rack.after_reply"
11
12
 
12
13
  class Utils
14
+ # Fetch the HTTP request method from the request.
15
+ #
16
+ # The request class is configurable, so reading the method can raise.
17
+ # Log and return nil in that case, leaving it to the caller to skip
18
+ # whatever it needed the method for.
19
+ #
20
+ # @param request [Rack::Request] Request object.
21
+ # @return [String, NilClass]
22
+ def self.request_method_from(request)
23
+ request.request_method
24
+ rescue => error
25
+ Appsignal.internal_logger.error(
26
+ "Exception while fetching the HTTP request method: #{error.class}: #{error}"
27
+ )
28
+ nil
29
+ end
30
+
31
+ # Fetch a value that describes the request, named after the method that
32
+ # reads it.
33
+ #
34
+ # The request class is configurable, so reading from the request can
35
+ # raise. Log and return nil in that case, leaving it to the caller to skip
36
+ # whatever it needed the value for.
37
+ #
38
+ # @param request [Rack::Request] Request object.
39
+ # @param name [Symbol] Name of the method that reads the value.
40
+ # @return [Object, NilClass]
41
+ def self.request_value_from(request, name)
42
+ request.public_send(name)
43
+ rescue => error
44
+ Appsignal.internal_logger.error(
45
+ "Exception while fetching the HTTP request #{name}: #{error.class}: #{error}"
46
+ )
47
+ nil
48
+ end
49
+
13
50
  # Fetch the queue start time from the request environment.
14
51
  #
15
52
  # @since 3.11.0
@@ -53,14 +90,14 @@ module Appsignal
53
90
  # TODO: Remove in next major/minor version
54
91
  transaction.set_metadata("path", request_path)
55
92
 
56
- request_method = request_method_for(request)
93
+ request_method = Appsignal::Rack::Utils.request_method_from(request)
57
94
  if request_method
58
95
  transaction.set_metadata("request_method", request_method)
59
96
  # TODO: Remove in next major/minor version
60
97
  transaction.set_metadata("method", request_method)
61
98
  end
62
99
 
63
- transaction.add_params { params_for(request) }
100
+ transaction.add_request_payload { params_for(request) }
64
101
  transaction.add_session_data { session_data_for(request) }
65
102
  transaction.add_headers do
66
103
  request.env if request.respond_to?(:env)
@@ -84,15 +121,6 @@ module Appsignal
84
121
  nil
85
122
  end
86
123
 
87
- def request_method_for(request)
88
- request.request_method
89
- rescue => error
90
- Appsignal.internal_logger.error(
91
- "Exception while fetching the HTTP request method: #{error.class}: #{error}"
92
- )
93
- nil
94
- end
95
-
96
124
  def session_data_for(request)
97
125
  return unless request.respond_to?(:session)
98
126
 
@@ -61,6 +61,10 @@ module Appsignal
61
61
  @empty
62
62
  end
63
63
 
64
+ # The sample-data category this holds (e.g. `:params`, `:request_payload`).
65
+ # Used to name the data in log messages.
66
+ attr_reader :key
67
+
64
68
  protected
65
69
 
66
70
  attr_reader :blocks
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Transaction
5
+ # @!visibility private
6
+ #
7
+ # The interface every transaction backend implements. `Appsignal::Backends
8
+ # .transaction` picks a concrete backend per mode -- ExtensionBackend in
9
+ # agent mode, OpenTelemetryBackend in collector mode. This base documents the
10
+ # contract; a backend that leaves a method unimplemented raises here.
11
+ class BaseBackend
12
+ # Instrumented events.
13
+ def start_event(opentelemetry_kind: nil, opentelemetry_scope: nil)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def finish_event(_name, _title, _body, _body_format)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def record_event( # rubocop:disable Metrics/ParameterLists
22
+ _name, _title, _body, _body_format, _duration,
23
+ opentelemetry_kind: nil, opentelemetry_scope: nil
24
+ )
25
+ raise NotImplementedError
26
+ end
27
+
28
+ # Transaction metadata.
29
+ def set_action(_action)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def set_namespace(_namespace)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def set_metadata(_key, _value)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def set_queue_start(_start)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ # OpenTelemetry span attributes, set on whichever span the backend is
46
+ # currently recording. Only meaningful in collector mode; agent mode has
47
+ # no span to hang them on.
48
+ def set_attributes(_attributes)
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # Maps each logical params channel (`:params`, `:request_payload`,
53
+ # `:function_parameters`) to the storage bucket it lands in. Channels that
54
+ # share a bucket merge into one `SampleData` object on the transaction;
55
+ # distinct buckets stay separate. The bucket name is also the sample-data
56
+ # key `set_sample_data` receives for that bucket, so it must be a key this
57
+ # backend's `set_sample_data` knows how to store.
58
+ def params_mapping
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # Sample data (params, session, tags, ...), breadcrumbs and errors.
63
+ def set_sample_data(_key, _data)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ def add_breadcrumb(_breadcrumb)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ def set_error(_class_name, _message, _backtrace, _causes, _root_cause_missing)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ # Whether the backend can hold more than one error on a single
76
+ # transaction. When it can't (agent mode), the Transaction reports the
77
+ # extra errors as duplicate transactions instead.
78
+ def supports_multiple_errors?
79
+ raise NotImplementedError
80
+ end
81
+
82
+ # Lifecycle.
83
+ def finish
84
+ raise NotImplementedError
85
+ end
86
+
87
+ def complete
88
+ raise NotImplementedError
89
+ end
90
+
91
+ def discard
92
+ raise NotImplementedError
93
+ end
94
+
95
+ # Only used when `supports_multiple_errors?` is false (agent mode).
96
+ # Backends that support multiple errors never duplicate and leave this
97
+ # unimplemented.
98
+ def duplicate(_new_transaction_id)
99
+ raise NotImplementedError
100
+ end
101
+
102
+ def to_json # rubocop:disable Lint/ToJSON
103
+ raise NotImplementedError
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Transaction
5
+ # @!visibility private
6
+ #
7
+ # The transaction backend used in agent mode. Wraps a per-transaction
8
+ # handle on the C extension (`Appsignal::Extension::Transaction`) and
9
+ # forwards every call to it.
10
+ #
11
+ # In agent mode `Appsignal::Backends.transaction` returns this class;
12
+ # `Appsignal::Transaction#initialize` instantiates one and stores it in
13
+ # `@backend`.
14
+ class ExtensionBackend < BaseBackend
15
+ # rubocop:disable Layout/LineLength
16
+ BACKTRACE_REGEX =
17
+ %r{(?<gem>[\w-]+ \(.+\) )?(?<path>:?/?\w+?.+?):(?<line>:?\d+)(?::in `(?<method>.+)')?$}.freeze
18
+ # rubocop:enable Layout/LineLength
19
+
20
+ # @!visibility private
21
+ attr_writer :breadcrumbs
22
+
23
+ # The `opentelemetry_*` keyword arguments (context, scope, kind and
24
+ # relationship) shape the OpenTelemetry span in collector mode. Agent mode
25
+ # has no notion of them, so they are accepted and ignored. They are listed
26
+ # explicitly, rather than swallowed with `**`, so an unexpected keyword
27
+ # still raises, matching the OpenTelemetry backend.
28
+ # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument
29
+ def initialize(
30
+ transaction_id,
31
+ namespace,
32
+ handle: nil,
33
+ opentelemetry_context: nil,
34
+ opentelemetry_scope: nil,
35
+ opentelemetry_kind: nil,
36
+ opentelemetry_relationship: nil
37
+ )
38
+ super()
39
+ @handle = handle ||
40
+ Appsignal::Extension.start_transaction(transaction_id, namespace, 0) ||
41
+ Appsignal::Extension::MockTransaction.new
42
+ @breadcrumbs = []
43
+ end
44
+ # rubocop:enable Metrics/ParameterLists, Lint/UnusedMethodArgument
45
+
46
+ # Agent mode has no span kind or instrumentation scope;
47
+ # `opentelemetry_kind` and `opentelemetry_scope` are ignored here.
48
+ def start_event(opentelemetry_kind: nil, opentelemetry_scope: nil) # rubocop:disable Lint/UnusedMethodArgument
49
+ @handle.start_event(0)
50
+ end
51
+
52
+ def finish_event(name, title, body, body_format)
53
+ @handle.finish_event(name, title, body, body_format, 0)
54
+ end
55
+
56
+ # Agent mode has no span kind or instrumentation scope;
57
+ # `opentelemetry_kind` and `opentelemetry_scope` are ignored here.
58
+ def record_event(name, title, body, body_format, duration, opentelemetry_kind: nil, opentelemetry_scope: nil, opentelemetry_attributes: nil) # rubocop:disable Lint/UnusedMethodArgument, Metrics/ParameterLists, Layout/LineLength
59
+ @handle.record_event(name, title, body, body_format, duration, 0)
60
+ end
61
+
62
+ def set_action(action)
63
+ @handle.set_action(action)
64
+ end
65
+
66
+ def set_namespace(namespace)
67
+ @handle.set_namespace(namespace)
68
+ end
69
+
70
+ def set_queue_start(start)
71
+ @handle.set_queue_start(start)
72
+ end
73
+
74
+ def set_metadata(key, value)
75
+ @handle.set_metadata(key, value)
76
+ end
77
+
78
+ # Agent mode has no OpenTelemetry spans, so there is nothing to set the
79
+ # attributes on. A transaction event has no attribute equivalent in the
80
+ # agent protocol either, so they are dropped rather than stored somewhere
81
+ # else.
82
+ def set_attributes(_attributes)
83
+ end
84
+
85
+ # The agent has a single params slot, so every params channel maps to one
86
+ # `:params` bucket. The transaction merges the channels into it, and only
87
+ # the `:params` key ever reaches `set_sample_data`.
88
+ PARAMS_MAPPING = {
89
+ :params => :params,
90
+ :request_payload => :params,
91
+ :function_parameters => :params,
92
+ :query_parameters => :params
93
+ }.freeze
94
+
95
+ def params_mapping
96
+ PARAMS_MAPPING
97
+ end
98
+
99
+ # `data` is a raw Ruby Hash/Array; the C extension wants a `Data` object,
100
+ # so serialize it here (mirrors how `set_error` serializes its backtrace).
101
+ def set_sample_data(key, data)
102
+ @handle.set_sample_data(key, Appsignal::Utils::Data.generate(data))
103
+ end
104
+
105
+ # Buffer breadcrumbs, keeping the last `BREADCRUMB_LIMIT`, and flush them as
106
+ # sample data on completion.
107
+ def add_breadcrumb(breadcrumb)
108
+ @breadcrumbs.push(breadcrumb)
109
+ @breadcrumbs = @breadcrumbs.last(Appsignal::Transaction::BREADCRUMB_LIMIT)
110
+ end
111
+
112
+ # Serializes the backtrace to a C-extension `Data` object and records the
113
+ # error, then flushes the causes as `error_causes` sample data in the
114
+ # agent's first-line shape.
115
+ def set_error(class_name, message, backtrace, causes, root_cause_missing)
116
+ backtrace_data =
117
+ if backtrace
118
+ Appsignal::Utils::Data.generate(backtrace)
119
+ else
120
+ Appsignal::Extension.data_array_new
121
+ end
122
+ @handle.set_error(class_name, message, backtrace_data)
123
+
124
+ set_sample_data("error_causes", error_causes_sample_data(causes, root_cause_missing))
125
+ end
126
+
127
+ def finish
128
+ @handle.finish(0)
129
+ end
130
+
131
+ def complete
132
+ unless @breadcrumbs.empty?
133
+ @handle.set_sample_data("breadcrumbs", Appsignal::Utils::Data.generate(@breadcrumbs))
134
+ end
135
+ @handle.complete
136
+ end
137
+
138
+ # Discarding in agent mode drops the transaction: the extension handle is
139
+ # simply abandoned and never told to complete, so nothing is sent. There
140
+ # is no `ignore_subtrace` concept on the agent path. This mirrors the
141
+ # pre-backend behavior, where `Transaction#complete` returned before
142
+ # touching the handle on a discarded transaction.
143
+ def discard
144
+ end
145
+
146
+ # The extension transaction holds a single error, so the Transaction
147
+ # reports additional errors as duplicate transactions instead.
148
+ def supports_multiple_errors?
149
+ false
150
+ end
151
+
152
+ def duplicate(new_transaction_id)
153
+ self.class.new(
154
+ new_transaction_id, nil, :handle => @handle.duplicate(new_transaction_id)
155
+ ).tap { |backend| backend.breadcrumbs = @breadcrumbs.dup }
156
+ end
157
+
158
+ def to_json # rubocop:disable Lint/ToJSON
159
+ @handle.to_json
160
+ end
161
+
162
+ private
163
+
164
+ # Projects the neutral causes to the agent's first-line shape. A truncated
165
+ # chain marks its last entry as not the root cause.
166
+ def error_causes_sample_data(causes, root_cause_missing)
167
+ sample_data = causes.map do |cause|
168
+ {
169
+ :name => cause[:name],
170
+ :message => cause[:message],
171
+ :first_line => first_formatted_backtrace_line(cause[:backtrace])
172
+ }
173
+ end
174
+ sample_data.last[:is_root_cause] = false if root_cause_missing && sample_data.any?
175
+ sample_data
176
+ end
177
+
178
+ # Parses the first backtrace line into the fields the UI links on (gem,
179
+ # path, line, method), with the path made relative to the app root.
180
+ def first_formatted_backtrace_line(backtrace)
181
+ first_line = backtrace&.first
182
+ return unless first_line
183
+
184
+ captures = BACKTRACE_REGEX.match(first_line)
185
+ return unless captures
186
+
187
+ captures.named_captures
188
+ .merge("original" => first_line)
189
+ .tap do |c|
190
+ config = Appsignal.config
191
+ c["gem"] = c["gem"]&.strip
192
+ root_path = config.root_path
193
+ if c["path"].start_with?(root_path)
194
+ c["path"].delete_prefix!(root_path)
195
+ c["path"].delete_prefix!("/")
196
+ end
197
+ c["revision"] = config[:revision]
198
+ c["line"] = c["line"].to_i
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end