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
@@ -2,20 +2,118 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
+ # @!visibility private
6
+ #
7
+ # Reads and writes W3C trace context the way OpenTelemetry's aws-sdk
8
+ # instrumentation does: as SQS message attributes, using the global
9
+ # propagator. Staying wire-equivalent means that if both AppSignal and
10
+ # OpenTelemetry's aws-sdk instrumentation are active, one simply shadows the
11
+ # other rather than corrupting the carrier. Collector mode only.
12
+ module ShoryukenTraceContext
13
+ module_function
14
+
15
+ # SQS allows at most 10 message attributes per message. Mirror
16
+ # OpenTelemetry and skip propagation rather than risk the enqueue failing
17
+ # when the user already fills the slots.
18
+ MAX_MESSAGE_ATTRIBUTES = 10
19
+
20
+ # Writes each trace header as an SQS message attribute, matching the shape
21
+ # OpenTelemetry's aws-sdk instrumentation injects on send.
22
+ module MessageAttributeSetter
23
+ def self.set(carrier, key, value)
24
+ return if carrier.length >= MAX_MESSAGE_ATTRIBUTES
25
+
26
+ carrier[key] = { :string_value => value, :data_type => "String" }
27
+ end
28
+ end
29
+
30
+ # Reads a trace header back out of a message attribute. Works both for the
31
+ # plain hash we inject and for the `Aws::SQS::Types::MessageAttributeValue`
32
+ # struct delivered on receive, since both respond to `[:string_value]` /
33
+ # `[:data_type]`.
34
+ module MessageAttributeGetter
35
+ def self.get(carrier, key)
36
+ attribute = carrier[key]
37
+ attribute[:string_value] if attribute && attribute[:data_type] == "String"
38
+ end
39
+ end
40
+
41
+ # Read the incoming context off a message's SQS message attributes so the
42
+ # transaction links back to the enqueuer. Returns an
43
+ # `OpenTelemetry::Context`, or `nil` outside collector mode.
44
+ def extract(message_attributes)
45
+ Appsignal::OpenTelemetry.if_started do
46
+ ::OpenTelemetry.propagation.extract(
47
+ message_attributes || {},
48
+ :getter => MessageAttributeGetter
49
+ )
50
+ end
51
+ end
52
+
53
+ # Write the current trace context into the outgoing send `options`.
54
+ # Injects into a scratch carrier first and merges it into the message
55
+ # attributes only when something was written, so an enqueue with no active
56
+ # span (no transaction, or outside collector mode) leaves the options
57
+ # untouched -- a transparent pass-through.
58
+ def inject(options)
59
+ Appsignal::OpenTelemetry.if_started do
60
+ carrier = {}
61
+ ::OpenTelemetry.propagation.inject(carrier, :setter => MessageAttributeSetter)
62
+ next if carrier.empty?
63
+
64
+ options[:message_attributes] = (options[:message_attributes] || {}).merge(carrier)
65
+ end
66
+ end
67
+ end
68
+
5
69
  # @!visibility private
6
70
  class ShoryukenMiddleware
7
71
  def call(worker_instance, queue, sqs_msg, body, &block)
8
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
72
+ batch = sqs_msg.is_a?(Array)
73
+ # How many messages this call covers, which is only reported for a batch.
74
+ batch_size = sqs_msg.size if batch
75
+
76
+ # Read the incoming trace context off the message so the transaction
77
+ # links back to the enqueuer. A batch carries messages from multiple
78
+ # traces with no single parent, so only single messages link back.
79
+ # No-op outside collector mode.
80
+ context = ShoryukenTraceContext.extract(sqs_msg.message_attributes) unless batch
81
+
82
+ transaction = Appsignal::Transaction.create(
83
+ Appsignal::Transaction::BACKGROUND_JOB,
84
+ :opentelemetry_context => context,
85
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION],
86
+ :opentelemetry_kind => :consumer,
87
+ :opentelemetry_relationship => :both
88
+ )
89
+ # Describes this span as a job being performed. The messaging system is
90
+ # what the trace timeline reads to recognize background job work.
91
+ # Shoryuken runs on Amazon SQS, so it takes the `aws_sqs` value the
92
+ # OpenTelemetry semantic conventions define for it.
93
+ transaction.add_opentelemetry_attributes(
94
+ Appsignal::OpenTelemetry::Messaging.perform_attributes(
95
+ "aws_sqs", :destination => queue, :batch_size => batch_size
96
+ )
97
+ )
9
98
 
10
- Appsignal.instrument("perform_job.shoryuken", &block)
99
+ Appsignal.instrument(
100
+ "perform_job.shoryuken",
101
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION]
102
+ ) do
103
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
104
+ Appsignal::OpenTelemetry::Messaging.perform_attributes(
105
+ "aws_sqs", :destination => queue, :batch_size => batch_size
106
+ )
107
+ )
108
+ block.call
109
+ end
11
110
  rescue Exception => error
12
111
  transaction.set_error(error)
13
112
  raise
14
113
  ensure
15
- batch = sqs_msg.is_a?(Array)
16
114
  attributes = fetch_attributes(batch, sqs_msg)
17
115
  transaction.set_action_if_nil("#{worker_instance.class.name}#perform")
18
- transaction.add_params_if_nil { fetch_args(batch, sqs_msg, body) }
116
+ transaction.add_function_parameters_if_nil { fetch_args(batch, sqs_msg, body) }
19
117
  transaction.add_tags(attributes)
20
118
  transaction.add_tags("queue" => queue)
21
119
  transaction.add_tags("batch" => true) if batch
@@ -73,7 +171,9 @@ module Appsignal
73
171
  end
74
172
 
75
173
  # Shoryuken client middleware that records an `enqueue.shoryuken` event so
76
- # the enqueue shows up under the active transaction.
174
+ # the enqueue shows up under the active transaction (both modes), and in
175
+ # collector mode writes the current trace context onto the outgoing message
176
+ # so the job that later performs links back to it.
77
177
  #
78
178
  # Like all AppSignal events, this only records when there's an active
79
179
  # transaction (e.g. enqueuing from within a web request or another job). An
@@ -81,13 +181,36 @@ module Appsignal
81
181
  #
82
182
  # @!visibility private
83
183
  class ShoryukenClientMiddleware
84
- def call(options, &block)
184
+ def call(options)
185
+ # When enqueue instrumentation is disabled, drop the trace context along
186
+ # with the event. Without an enqueue event there is no producer span, so
187
+ # the context we would write is that of whatever span is current, such as
188
+ # the surrounding web request. The job that performs later would then
189
+ # link back to a span that is not a producer.
190
+ return yield if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
191
+
85
192
  # Under Active Job the enqueue is already recorded as an
86
- # `enqueue.active_job` event, so skip recording it again here.
87
- return yield if Appsignal::Transaction.current? &&
88
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
193
+ # `enqueue.active_job` event, so skip recording it again here. The trace
194
+ # context is still injected so the performed job links back.
195
+ if Appsignal::Transaction.current? &&
196
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
197
+ ShoryukenTraceContext.inject(options)
198
+ return yield
199
+ end
89
200
 
90
- Appsignal.instrument("enqueue.shoryuken", enqueue_title(options), &block)
201
+ Appsignal.instrument(
202
+ "enqueue.shoryuken",
203
+ enqueue_title(options),
204
+ :opentelemetry_kind => :producer,
205
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION]
206
+ ) do
207
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
208
+ Appsignal::OpenTelemetry::Messaging
209
+ .enqueue_attributes("aws_sqs", :destination => queue_name(options))
210
+ )
211
+ ShoryukenTraceContext.inject(options)
212
+ yield
213
+ end
91
214
  end
92
215
 
93
216
  private
@@ -99,8 +222,13 @@ module Appsignal
99
222
  worker_class = options.dig(:message_attributes, "shoryuken_class", :string_value)
100
223
  return "enqueue #{worker_class} job" if worker_class
101
224
 
102
- queue = options[:queue_url].to_s.split("/").last
103
- "enqueue on #{queue}"
225
+ "enqueue on #{queue_name(options)}"
226
+ end
227
+
228
+ # The queue a message is being sent to, which SQS identifies by a URL whose
229
+ # last segment is the queue's name.
230
+ def queue_name(options)
231
+ options[:queue_url].to_s.split("/").last
104
232
  end
105
233
  end
106
234
  end
@@ -38,10 +38,18 @@ module Appsignal
38
38
  # Sidekiq error outside of the middleware scope.
39
39
  # Can be a job JSON parse error or some other error happening in
40
40
  # Sidekiq.
41
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
41
+ transaction = Appsignal::Transaction.create(
42
+ Appsignal::Transaction::BACKGROUND_JOB,
43
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION],
44
+ :opentelemetry_kind => :consumer,
45
+ :opentelemetry_relationship => :both
46
+ )
47
+ transaction.add_opentelemetry_attributes(
48
+ Appsignal::OpenTelemetry::Messaging.perform_attributes("sidekiq")
49
+ )
42
50
  transaction.set_action_if_nil("SidekiqInternal")
43
51
  transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
44
- transaction.add_params_if_nil(:jobstr => sidekiq_context[:jobstr])
52
+ transaction.add_function_parameters_if_nil(:jobstr => sidekiq_context[:jobstr])
45
53
  transaction.set_error(exception)
46
54
  end
47
55
 
@@ -98,24 +106,52 @@ module Appsignal
98
106
  end
99
107
  end
100
108
 
101
- # Sidekiq client middleware that runs on enqueue. Records an
102
- # `enqueue.sidekiq` event so the enqueue shows up under the active
103
- # transaction.
109
+ # Client middleware that runs on enqueue. Records an `enqueue.sidekiq`
110
+ # event so the enqueue shows up under the active transaction (both modes),
111
+ # and in collector mode writes the trace context onto the job hash so the
112
+ # job that later performs links back to it.
104
113
  #
105
114
  # Like all AppSignal events, this only records when there's an active
106
- # transaction (e.g. enqueuing from within a web request or another job). An
107
- # enqueue with no transaction is a transparent pass-through.
115
+ # transaction (e.g. enqueuing from within a web request or another job).
116
+ # An enqueue with no transaction is a transparent pass-through.
108
117
  #
109
118
  # @!visibility private
110
119
  class SidekiqClientMiddleware
111
- def call(_worker_class, job, _queue, _redis_pool, &block)
120
+ def call(_worker_class, job, _queue, _redis_pool)
121
+ # When enqueue instrumentation is disabled, drop the trace context along
122
+ # with the event. Without an enqueue event there is no producer span, so
123
+ # the context we would write is that of whatever span is current, such as
124
+ # the surrounding web request. The job that performs later would then
125
+ # link back to a span that is not a producer.
126
+ return yield if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
127
+
112
128
  # Under Active Job the enqueue is already recorded as an
113
- # `enqueue.active_job` event, so skip recording it again here.
114
- return yield if Appsignal::Transaction.current? &&
115
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
129
+ # `enqueue.active_job` event, so skip recording it again here. The trace
130
+ # context is still injected so the performed job links back.
131
+ if Appsignal::Transaction.current? &&
132
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
133
+ Appsignal::OpenTelemetry.inject_context(job)
134
+ return yield
135
+ end
116
136
 
117
137
  title = "enqueue #{SidekiqActionName.parse_action_name(job)} job"
118
- Appsignal.instrument("enqueue.sidekiq", title, &block)
138
+ Appsignal.instrument(
139
+ "enqueue.sidekiq",
140
+ title,
141
+ :opentelemetry_kind => :producer,
142
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION]
143
+ ) do
144
+ # Describes this span as a job being enqueued. The messaging system
145
+ # is what the trace timeline reads to recognize background job work,
146
+ # and `sidekiq` is the value OpenTelemetry's own Sidekiq
147
+ # instrumentation uses.
148
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
149
+ Appsignal::OpenTelemetry::Messaging
150
+ .enqueue_attributes("sidekiq", :destination => job["queue"])
151
+ )
152
+ Appsignal::OpenTelemetry.inject_context(job)
153
+ yield
154
+ end
119
155
  end
120
156
  end
121
157
 
@@ -126,7 +162,7 @@ module Appsignal
126
162
  EXCLUDED_JOB_KEYS = %w[
127
163
  args backtrace class created_at enqueued_at error_backtrace error_class
128
164
  error_message failed_at jid retried_at retry wrapped cattr tags retry_for
129
- unique_for
165
+ unique_for traceparent tracestate __otel_headers
130
166
  ].freeze
131
167
 
132
168
  def self.sidekiq8?
@@ -138,7 +174,19 @@ module Appsignal
138
174
  def call(_worker, item, _queue, &block)
139
175
  job_status = nil
140
176
  action_name = formatted_action_name(item)
141
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
177
+ # Read trace context off the job so the transaction links back to the
178
+ # enqueuer. No-op outside collector mode.
179
+ transaction = Appsignal::Transaction.create(
180
+ Appsignal::Transaction::BACKGROUND_JOB,
181
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(item),
182
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION],
183
+ :opentelemetry_kind => :consumer,
184
+ :opentelemetry_relationship => :both
185
+ )
186
+ transaction.add_opentelemetry_attributes(
187
+ Appsignal::OpenTelemetry::Messaging
188
+ .perform_attributes("sidekiq", :destination => item["queue"])
189
+ )
142
190
  transaction.set_action_if_nil(action_name)
143
191
 
144
192
  formatted_metadata(item).each do |key, value|
@@ -146,13 +194,22 @@ module Appsignal
146
194
  end
147
195
 
148
196
  begin
149
- Appsignal.instrument "perform_job.sidekiq", &block
197
+ Appsignal.instrument(
198
+ "perform_job.sidekiq",
199
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION]
200
+ ) do
201
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
202
+ Appsignal::OpenTelemetry::Messaging
203
+ .perform_attributes("sidekiq", :destination => item["queue"])
204
+ )
205
+ block.call
206
+ end
150
207
  rescue Exception => exception
151
208
  job_status = :failed
152
209
  raise exception
153
210
  ensure
154
211
  if transaction
155
- transaction.add_params_if_nil { parse_arguments(item) }
212
+ transaction.add_function_parameters_if_nil { parse_arguments(item) }
156
213
  enqueued_at = item["enqueued_at"]
157
214
  queue_start =
158
215
  if self.class.sidekiq8?
@@ -10,20 +10,63 @@ module Appsignal
10
10
  if has_parent_transaction
11
11
  Appsignal::Transaction.current
12
12
  else
13
- Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
13
+ # Read the incoming trace context off the request headers so the
14
+ # transaction continues the upstream trace. No-op outside collector
15
+ # mode. Webmachine isn't Rack: `request.headers` is a case-insensitive
16
+ # `Webmachine::Headers`, so the default getter reads it directly.
17
+ Appsignal::Transaction.create(
18
+ Appsignal::Transaction::HTTP_REQUEST,
19
+ :opentelemetry_context => Appsignal::OpenTelemetry.if_started do
20
+ ::OpenTelemetry.propagation.extract(request.headers)
21
+ end,
22
+ :opentelemetry_scope => ["appsignal-ruby/webmachine", Appsignal::VERSION]
23
+ )
14
24
  end
15
25
 
26
+ unless has_parent_transaction
27
+ # Describes the transaction's span as an incoming HTTP request.
28
+ # Together with the SERVER span kind the transaction already carries,
29
+ # this is what the trace timeline reads to recognize a web request.
30
+ # Set here, where the transaction is created, so they land on the
31
+ # transaction's own span rather than on the event started below.
32
+ #
33
+ # Webmachine isn't Rack: the path, scheme and query string come off the
34
+ # request's `URI` rather than from Rack's readers. The request's own
35
+ # `query` is the parsed form, so the URI is where the string itself is.
36
+ transaction.add_opentelemetry_attributes(
37
+ Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
38
+ :method => request.method,
39
+ :path => request.uri&.path,
40
+ :scheme => request.uri&.scheme,
41
+ :query => request.uri&.query
42
+ )
43
+ )
44
+ end
45
+
16
46
  begin
17
- transaction.add_params_if_nil { request.query }
47
+ transaction.add_query_parameters_if_nil { request.query }
18
48
  transaction.add_headers_if_nil { request.headers if request.respond_to?(:headers) }
19
49
 
20
- Appsignal.instrument("process_action.webmachine") do
50
+ Appsignal.instrument(
51
+ "process_action.webmachine",
52
+ :opentelemetry_scope => ["appsignal-ruby/webmachine", Appsignal::VERSION]
53
+ ) do
21
54
  super
22
55
  end
23
56
  ensure
24
57
  transaction.set_action_if_nil("#{resource.class.name}##{request.method}")
25
58
 
26
- Appsignal::Transaction.complete_current! unless has_parent_transaction
59
+ unless has_parent_transaction
60
+ # Describes the response on the transaction's span, which the
61
+ # semantic conventions ask for whenever a response was sent. The
62
+ # event above has closed by now, so this lands on the transaction's
63
+ # own span.
64
+ transaction.add_opentelemetry_attributes(
65
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(response.code)
66
+ )
67
+
68
+ Appsignal::Transaction.complete_current!
69
+ end
27
70
  end
28
71
  end
29
72
 
@@ -20,7 +20,8 @@ module Appsignal
20
20
  Padrino.before_load do
21
21
  Padrino.use Appsignal::Rack::EventMiddleware
22
22
  Padrino.use Appsignal::Rack::SinatraBaseInstrumentation,
23
- :instrument_event_name => "process_action.padrino"
23
+ :instrument_event_name => "process_action.padrino",
24
+ :opentelemetry_scope => ["appsignal-ruby/padrino", Appsignal::VERSION]
24
25
  end
25
26
  end
26
27
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Logger < ::Logger
5
+ # @!visibility private
6
+ #
7
+ # Routes Appsignal::Logger emits through the AppSignal C-extension,
8
+ # which forwards them to the agent. This is the default backend used
9
+ # when collector mode is not active.
10
+ module ExtensionBackend
11
+ class << self
12
+ def emit(group, severity, format, message, attributes)
13
+ Appsignal::Extension.log(
14
+ group,
15
+ SEVERITY_MAP.fetch(severity, 0),
16
+ format,
17
+ message,
18
+ Appsignal::Utils::Data.generate(attributes)
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal/opentelemetry/attributes"
4
+
5
+ module Appsignal
6
+ class Logger < ::Logger
7
+ # @!visibility private
8
+ #
9
+ # Routes Appsignal::Logger emits through the OpenTelemetry logs SDK
10
+ # using the logger provider configured at `Appsignal.start` time when
11
+ # collector mode is active.
12
+ #
13
+ # Each emit attaches two well-known attributes that the AppSignal
14
+ # collector consumes:
15
+ #
16
+ # - `appsignal.group` — overrides the collector's default
17
+ # `service.name`-based grouping with the logger's `group` argument.
18
+ # - `appsignal.format` — the lowercase parse-format name
19
+ # (`plaintext`/`logfmt`/`json`/`autodetect`) the processor uses to
20
+ # extract structured attributes from the message body.
21
+ module OpenTelemetryBackend
22
+ # Maps Ruby `::Logger` severities to OTel SeverityNumber + the
23
+ # human-readable severity text.
24
+ OTEL_SEVERITY_MAP = {
25
+ ::Logger::DEBUG => [5, "DEBUG"],
26
+ ::Logger::INFO => [9, "INFO"],
27
+ ::Logger::WARN => [13, "WARN"],
28
+ ::Logger::ERROR => [17, "ERROR"],
29
+ ::Logger::FATAL => [21, "FATAL"]
30
+ }.freeze
31
+
32
+ # Maps the integer parse-format flag on `Appsignal::Logger` to the
33
+ # lowercase string the AppSignal collector and processor share.
34
+ FORMAT_NAMES = {
35
+ Appsignal::Logger::PLAINTEXT => "plaintext",
36
+ Appsignal::Logger::LOGFMT => "logfmt",
37
+ Appsignal::Logger::JSON => "json",
38
+ Appsignal::Logger::AUTODETECT => "autodetect"
39
+ }.freeze
40
+
41
+ class << self
42
+ def emit(group, severity, format, message, attributes)
43
+ number, text = OTEL_SEVERITY_MAP.fetch(severity, [0, nil])
44
+ otel_attributes = Appsignal::OpenTelemetry::Attributes.format(attributes)
45
+ otel_attributes["appsignal.group"] = group.to_s
46
+ otel_attributes["appsignal.format"] = FORMAT_NAMES.fetch(format, "autodetect")
47
+ logger.on_emit(
48
+ :severity_number => number,
49
+ :severity_text => text,
50
+ :body => message,
51
+ :attributes => otel_attributes
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ # Resolve the OTel logger on each emit. The logger provider caches it
58
+ # by name, so this is a registry lookup rather than a rebuild, and it
59
+ # always reflects the currently configured provider.
60
+ def logger
61
+ ::OpenTelemetry.logger_provider.logger(:name => "appsignal-logger")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logger"
4
- require "set"
5
4
 
6
5
  module Appsignal
7
6
  # Logger that flushes logs to the AppSignal logging service.
@@ -55,6 +54,8 @@ module Appsignal
55
54
  # @!visibility private
56
55
  AUTODETECT = 3
57
56
  # @!visibility private
57
+ FORMATS = [PLAINTEXT, LOGFMT, JSON, AUTODETECT].freeze
58
+ # @!visibility private
58
59
  SEVERITY_MAP = {
59
60
  DEBUG => 2,
60
61
  INFO => 3,
@@ -83,7 +84,7 @@ module Appsignal
83
84
  @group = group
84
85
  @level = level
85
86
  @silenced = false
86
- @format = format
87
+ @format = validated_format(format)
87
88
  @mutex = Mutex.new
88
89
  @default_attributes = attributes
89
90
  @appsignal_attributes = attributes
@@ -145,13 +146,7 @@ module Appsignal
145
146
 
146
147
  message = formatter.call(severity, Time.now, group, message) if formatter
147
148
 
148
- Appsignal::Extension.log(
149
- group,
150
- SEVERITY_MAP.fetch(severity, 0),
151
- @format,
152
- message.to_s,
153
- Appsignal::Utils::Data.generate(appsignal_attributes)
154
- )
149
+ Appsignal::Backends.logger.emit(group, severity, @format, message.to_s, appsignal_attributes)
155
150
 
156
151
  false
157
152
  end
@@ -299,5 +294,14 @@ module Appsignal
299
294
  ensure
300
295
  @appsignal_attributes = default_attributes
301
296
  end
297
+
298
+ def validated_format(format)
299
+ return format if FORMATS.include?(format)
300
+
301
+ Appsignal.internal_logger.warn(
302
+ "Unknown Appsignal::Logger format #{format.inspect}; falling back to AUTODETECT"
303
+ )
304
+ AUTODETECT
305
+ end
302
306
  end
303
307
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Metrics
5
+ # @!visibility private
6
+ #
7
+ # Routes custom metric helper calls through the AppSignal C-extension,
8
+ # which forwards them to the agent. This is the default backend used
9
+ # when collector mode is not active.
10
+ module ExtensionBackend
11
+ class << self
12
+ def set_gauge(name, value, tags)
13
+ Appsignal::Extension.set_gauge(
14
+ name.to_s,
15
+ value.to_f,
16
+ Appsignal::Utils::Data.generate(tags)
17
+ )
18
+ rescue RangeError
19
+ Appsignal.internal_logger
20
+ .warn("The gauge value '#{value}' for metric '#{name}' is too big")
21
+ end
22
+
23
+ def increment_counter(name, value, tags)
24
+ Appsignal::Extension.increment_counter(
25
+ name.to_s,
26
+ value.to_f,
27
+ Appsignal::Utils::Data.generate(tags)
28
+ )
29
+ rescue RangeError
30
+ Appsignal.internal_logger
31
+ .warn("The counter value '#{value}' for metric '#{name}' is too big")
32
+ end
33
+
34
+ def add_distribution_value(name, value, tags)
35
+ Appsignal::Extension.add_distribution_value(
36
+ name.to_s,
37
+ value.to_f,
38
+ Appsignal::Utils::Data.generate(tags)
39
+ )
40
+ rescue RangeError
41
+ Appsignal.internal_logger
42
+ .warn("The distribution value '#{value}' for metric '#{name}' is too big")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end