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
@@ -25,6 +25,11 @@ module Appsignal
25
25
  # another job). An enqueue with no active transaction is a transparent
26
26
  # pass-through.
27
27
  def self.enqueue_with_instrumentation(job, block)
28
+ # Skip the enqueue event when enqueue instrumentation is disabled.
29
+ if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
30
+ return block.call(job)
31
+ end
32
+
28
33
  # Under Active Job the enqueue is already recorded as an
29
34
  # `enqueue.active_job` event, so skip recording it again here.
30
35
  if Appsignal::Transaction.current? &&
@@ -32,7 +37,20 @@ module Appsignal
32
37
  return block.call(job)
33
38
  end
34
39
 
35
- Appsignal.instrument("enqueue.delayed_job", "enqueue #{enqueue_name(job)} job") do
40
+ Appsignal.instrument(
41
+ "enqueue.delayed_job",
42
+ "enqueue #{enqueue_name(job)} job",
43
+ :opentelemetry_kind => :producer,
44
+ :opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION]
45
+ ) do
46
+ # Describes this span as a job being enqueued. The messaging system
47
+ # is what the trace timeline reads to recognize background job work,
48
+ # and `delayed_job` is the value OpenTelemetry's own Delayed Job
49
+ # instrumentation uses.
50
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
51
+ Appsignal::OpenTelemetry::Messaging
52
+ .enqueue_attributes("delayed_job", :destination => queue_name(job))
53
+ )
36
54
  block.call(job)
37
55
  end
38
56
  end
@@ -43,6 +61,12 @@ module Appsignal
43
61
  # `enqueue Class#method job` rather than the bare `enqueue Class job`. We
44
62
  # accept that inconsistency so the enqueue and perform events stay tied to
45
63
  # the same name for the rare job that sets it.
64
+ # The queue a job is on. Not every Delayed Job backend has queues, so a
65
+ # job that does not know its queue is described without one.
66
+ def self.queue_name(job)
67
+ job.queue if job.respond_to?(:queue)
68
+ end
69
+
46
70
  def self.enqueue_name(job)
47
71
  payload = job.payload_object
48
72
  appsignal_name = extract_value(payload, :appsignal_name, nil)
@@ -53,10 +77,26 @@ module Appsignal
53
77
 
54
78
  def self.invoke_with_instrumentation(job, block)
55
79
  transaction =
56
- Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
80
+ Appsignal::Transaction.create(
81
+ Appsignal::Transaction::BACKGROUND_JOB,
82
+ :opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION],
83
+ :opentelemetry_kind => :consumer,
84
+ :opentelemetry_relationship => :both
85
+ )
86
+ transaction.add_opentelemetry_attributes(
87
+ Appsignal::OpenTelemetry::Messaging
88
+ .perform_attributes("delayed_job", :destination => queue_name(job))
89
+ )
57
90
 
58
91
  begin
59
- Appsignal.instrument("perform_job.delayed_job") do
92
+ Appsignal.instrument(
93
+ "perform_job.delayed_job",
94
+ :opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION]
95
+ ) do
96
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
97
+ Appsignal::OpenTelemetry::Messaging
98
+ .perform_attributes("delayed_job", :destination => queue_name(job))
99
+ )
60
100
  block.call(job)
61
101
  end
62
102
  rescue Exception => error
@@ -68,11 +108,11 @@ module Appsignal
68
108
  # ActiveJob
69
109
  job_data = payload.job_data
70
110
  transaction.set_action_if_nil("#{job_data["job_class"]}#perform")
71
- transaction.add_params_if_nil(job_data.fetch("arguments", {}))
111
+ transaction.add_function_parameters_if_nil(job_data.fetch("arguments", {}))
72
112
  else
73
113
  # Delayed Job
74
114
  transaction.set_action_if_nil(action_name_from_payload(payload, job.name))
75
- transaction.add_params_if_nil(extract_value(payload, :args, {}))
115
+ transaction.add_function_parameters_if_nil(extract_value(payload, :args, {}))
76
116
  end
77
117
 
78
118
  transaction.add_tags(
@@ -4,24 +4,48 @@ module Appsignal
4
4
  module Integrations
5
5
  # @!visibility private
6
6
  module DryMonitorIntegration
7
+ # The event's formatter says what kind of work the event is, such as ROM
8
+ # reporting a SQL query as a dry-monitor `"sql"` event, and which library
9
+ # the instrumentation is for. Both are immutable once the span exists, so
10
+ # they have to be set here at event start.
11
+ #
12
+ # dry-monitor is a notification bus, so an event arriving over it is not
13
+ # necessarily dry-monitor's own work. A formatter that knows better says
14
+ # so; anything else is attributed to dry-monitor.
7
15
  def instrument(event_id, payload = {}, &block)
8
- Appsignal::Transaction.current.start_event
9
-
10
- super
11
- ensure
12
16
  name = "#{event_id}.dry"
13
- event_name, body, body_format = Appsignal::EventFormatter.format(name, payload)
17
+ # An event a dedicated integration already records is not recorded a
18
+ # second time here.
19
+ return super unless Appsignal::EventFormatter.record?(name)
20
+
21
+ begin
22
+ Appsignal::Transaction.current.start_event(
23
+ :opentelemetry_kind => Appsignal::EventFormatter.opentelemetry_kind(name),
24
+ :opentelemetry_scope =>
25
+ Appsignal::EventFormatter.opentelemetry_scope(name) ||
26
+ ["appsignal-ruby/dry_monitor", Appsignal::VERSION]
27
+ )
28
+
29
+ super
30
+ ensure
31
+ event_name, body, body_format = Appsignal::EventFormatter.format(name, payload)
32
+ # Set while the event's span is still open, so the attributes land
33
+ # on the event rather than on the transaction.
34
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
35
+ Appsignal::EventFormatter.opentelemetry_attributes(name, payload)
36
+ )
14
37
 
15
- # dry-monitor reports an event under an id, such as `sql`, rather than
16
- # a name. A formatter names the event it knows about, and an event
17
- # without one is named after its id in the dry-monitor group. Either
18
- # way the name has a group, which is what an event is listed under.
19
- Appsignal::Transaction.current.finish_event(
20
- event_name || name,
21
- nil,
22
- body,
23
- body_format
24
- )
38
+ # dry-monitor reports an event under an id, such as `sql`, rather than
39
+ # a name. A formatter names the event it knows about, and an event
40
+ # without one is named after its id in the dry-monitor group. Either
41
+ # way the name has a group, which is what an event is listed under.
42
+ Appsignal::Transaction.current.finish_event(
43
+ event_name || name,
44
+ nil,
45
+ body,
46
+ body_format
47
+ )
48
+ end
25
49
  end
26
50
  end
27
51
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ # Excon middleware that writes trace context onto the outgoing request, so
6
+ # the called service joins this trace. The integration on the connection
7
+ # records the span; this middleware only injects.
8
+ #
9
+ # @!visibility private
10
+ class ExconMiddleware < ::Excon::Middleware::Base
11
+ def request_call(datum)
12
+ datum[:headers] ||= {}
13
+ # Inject from whatever span is current. The connection's client span is
14
+ # open around the whole request, so the written `traceparent` reflects
15
+ # the Excon client event. No-op outside collector mode.
16
+ Appsignal::OpenTelemetry.inject_context(datum[:headers])
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,16 +4,20 @@ module Appsignal
4
4
  module Integrations
5
5
  # @!visibility private
6
6
  module ExconIntegration
7
+ # The method of a request. Excon defaults it to GET further down, so the
8
+ # same default is applied here.
9
+ def self.method_for(datum)
10
+ datum[:method] || :get
11
+ end
12
+
7
13
  # The title of the event, built the way the Net::HTTP integration builds
8
14
  # its own: the request method and where the request went, without the
9
15
  # path, so paths stay out of event titles.
10
16
  #
11
17
  # Excon splits a request's data between the connection it is made on and
12
- # the call that makes it, so both are read to build this. Excon defaults
13
- # the method to GET itself, so the same default is applied here.
18
+ # the call that makes it, so both are read to build this.
14
19
  def self.title_for(datum)
15
- method = (datum[:method] || :get).to_s.upcase
16
- "#{method} #{datum[:scheme]}://#{datum[:host]}"
20
+ "#{method_for(datum).to_s.upcase} #{datum[:scheme]}://#{datum[:host]}"
17
21
  end
18
22
 
19
23
  def request(params = {})
@@ -38,18 +42,51 @@ module Appsignal
38
42
  # A pipelined request is the exception to this method being the whole of
39
43
  # a request. It returns before the response is read, so its event covers
40
44
  # only the sending.
41
- title = ExconIntegration.title_for(data.merge(params))
45
+ datum = data.merge(params)
46
+
47
+ Appsignal.instrument(
48
+ "request.excon",
49
+ ExconIntegration.title_for(datum),
50
+ :opentelemetry_kind => :client,
51
+ :opentelemetry_scope => ["appsignal-ruby/excon", Appsignal::VERSION]
52
+ ) do
53
+ # Describes the span as an outgoing HTTP request. Together with the
54
+ # CLIENT kind, this is what the trace timeline reads to recognize it
55
+ # as one.
56
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
57
+ Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
58
+ :method => ExconIntegration.method_for(datum),
59
+ :scheme => datum[:scheme],
60
+ :host => datum[:host],
61
+ :port => datum[:port],
62
+ :path => datum[:path]
63
+ )
64
+ )
65
+
66
+ response =
67
+ if Appsignal::Transaction.current?
68
+ # Excon retries a request, and follows a redirect, by calling this
69
+ # method again from inside the request it is retrying or
70
+ # following. Suppressing those means they count towards this event
71
+ # rather than becoming events of their own, so one request stays
72
+ # one event.
73
+ Appsignal::Transaction.current.suppress_http_client_events { super }
74
+ else
75
+ super
76
+ end
42
77
 
43
- Appsignal.instrument("request.excon", title) do
44
- if Appsignal::Transaction.current?
45
- # Excon retries a request, and follows a redirect, by calling this
46
- # method again from inside the request it is retrying or following.
47
- # Suppressing those means they count towards this event rather than
48
- # becoming events of their own, so one request stays one event.
49
- Appsignal::Transaction.current.suppress_http_client_events { super }
50
- else
51
- super
52
- end
78
+ # Describes the response on the same span as the request, which the
79
+ # semantic conventions ask for whenever one was received.
80
+ #
81
+ # A pipelined request returns the request data rather than a response,
82
+ # because its response has not been read yet, so there is no status to
83
+ # report for it.
84
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
85
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(
86
+ response.respond_to?(:status) ? response.status : nil
87
+ )
88
+ )
89
+ response
53
90
  end
54
91
  end
55
92
  end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
- # Faraday middleware that records each request as a `request.faraday` event
6
- # and suppresses the downstream HTTP client's own instrumentation, so the
7
- # request is recorded once rather than as nested Faraday + Net::HTTP (or
8
- # Excon) client events.
5
+ # Faraday middleware that records each request as a `request.faraday` client
6
+ # event, writes trace context onto the outgoing request so the called service
7
+ # joins this trace, and suppresses the downstream HTTP client's own
8
+ # instrumentation, so the request is recorded once rather than as nested
9
+ # Faraday + Net::HTTP (or Excon) client events.
9
10
  #
10
11
  # @!visibility private
11
12
  class FaradayMiddleware < ::Faraday::Middleware
@@ -16,17 +17,50 @@ module Appsignal
16
17
  # Net::HTTP's (scheme and host only), keeping paths out of event titles.
17
18
  Appsignal.instrument(
18
19
  "request.faraday",
19
- "#{http_method} #{uri.scheme}://#{uri.host}"
20
+ "#{http_method} #{uri.scheme}://#{uri.host}",
21
+ :opentelemetry_kind => :client,
22
+ :opentelemetry_scope => ["appsignal-ruby/faraday", Appsignal::VERSION]
20
23
  ) do
24
+ # Describes the span as an outgoing HTTP request. Together with the
25
+ # CLIENT kind, this is what the trace timeline reads to recognize it
26
+ # as one.
27
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
28
+ Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
29
+ :method => env[:method],
30
+ :scheme => uri.scheme,
31
+ :host => uri.host,
32
+ :port => uri.port,
33
+ :path => uri.path
34
+ )
35
+ )
36
+ # Write trace context onto the outgoing request so the called service
37
+ # joins this trace. Injected inside the instrument block, so the written
38
+ # `traceparent` reflects the Faraday client event's span. No-op outside
39
+ # collector mode. `env.request_headers` is the live outgoing header set
40
+ # and a valid carrier (it responds to `[]=`).
41
+ Appsignal::OpenTelemetry.inject_context(env.request_headers)
42
+
21
43
  # Faraday's default adapter is Net::HTTP, which AppSignal also
22
44
  # instruments. Suppress the adapter's own instrumentation so the
23
45
  # request appears once (as the Faraday event) rather than as nested
24
46
  # Faraday + Net::HTTP client events.
25
- if Appsignal::Transaction.current?
26
- Appsignal::Transaction.current.suppress_http_client_events { @app.call(env) }
27
- else
28
- @app.call(env)
29
- end
47
+ response =
48
+ if Appsignal::Transaction.current?
49
+ Appsignal::Transaction.current.suppress_http_client_events { @app.call(env) }
50
+ else
51
+ @app.call(env)
52
+ end
53
+
54
+ # Describes the response on the same span, which the semantic
55
+ # conventions ask for whenever one was received. The event is still
56
+ # open here, so it lands on the request's own span. The status is read
57
+ # off the environment rather than the returned response, because an
58
+ # adapter fills the response in later than it fills in the environment.
59
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
60
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(env[:status])
61
+ )
62
+
63
+ response
30
64
  end
31
65
  end
32
66
  end
@@ -37,8 +71,9 @@ module Appsignal
37
71
  # the build path is the only way to instrument every connection automatically.
38
72
  #
39
73
  # Just before the adapter (the innermost handler, where the request is sent)
40
- # it inserts `FaradayMiddleware`, which records the `request.faraday` event
41
- # and suppresses the downstream client. Skipped if it's already present.
74
+ # it inserts `FaradayMiddleware`, which records the `request.faraday` event,
75
+ # injects trace context, and suppresses the downstream client. Skipped if it's
76
+ # already present.
42
77
  #
43
78
  # @!visibility private
44
79
  module FaradayRackBuilderPatch
@@ -9,7 +9,34 @@ module Appsignal
9
9
  parsed_request_uri = uri.is_a?(URI) ? uri : uri_module.parse(uri.to_s)
10
10
  request_uri = "#{parsed_request_uri.scheme}://#{parsed_request_uri.host}"
11
11
 
12
- Appsignal.instrument("request.http_rb", "#{verb.upcase} #{request_uri}", &block)
12
+ Appsignal.instrument(
13
+ "request.http_rb",
14
+ "#{verb.to_s.upcase} #{request_uri}",
15
+ :opentelemetry_kind => :client,
16
+ :opentelemetry_scope => ["appsignal-ruby/http_rb", Appsignal::VERSION]
17
+ ) do
18
+ # Describes the span as an outgoing HTTP request. Together with the
19
+ # CLIENT kind, this is what the trace timeline reads to recognize it
20
+ # as one.
21
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
22
+ Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
23
+ :method => verb,
24
+ :scheme => parsed_request_uri.scheme,
25
+ :host => parsed_request_uri.host,
26
+ :port => parsed_request_uri.port,
27
+ :path => parsed_request_uri.path
28
+ )
29
+ )
30
+ # Describes the response on the same span, which the semantic
31
+ # conventions ask for whenever one was received. The event is still
32
+ # open here, so it lands on the request's own span. A request that
33
+ # followed redirects reports the status of the response it ended on.
34
+ block.call.tap do |response|
35
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
36
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(response&.code)
37
+ )
38
+ end
39
+ end
13
40
  end
14
41
 
15
42
  # The event is recorded at the request boundary, so a redirected request
@@ -32,6 +59,21 @@ module Appsignal
32
59
  HttpIntegration.instrument(verb, uri) { super }
33
60
  end
34
61
  end
62
+
63
+ # Trace context has to ride on each outgoing hop's headers, so it's
64
+ # injected at `HTTP::Client#perform` -- the single send chokepoint in both
65
+ # http5 and http6, called once per request and once per redirect hop --
66
+ # where the live request headers are reachable. The event stays at the
67
+ # request boundary above, so a redirected request is still a single event;
68
+ # this only propagates context, and every hop carries it. No-op outside
69
+ # collector mode. `req.headers` is the live outgoing header set and a valid
70
+ # carrier (it responds to `[]=`).
71
+ module ContextInjection
72
+ def perform(req, options)
73
+ Appsignal::OpenTelemetry.inject_context(req.headers)
74
+ super
75
+ end
76
+ end
35
77
  end
36
78
  end
37
79
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Appsignal
4
6
  class Hooks
5
7
  # @!visibility private
@@ -19,8 +21,25 @@ module Appsignal
19
21
  store = transaction.store("mongo_driver")
20
22
  store[event.request_id] = command
21
23
 
22
- # Start this event
23
- transaction.start_event
24
+ # Start this event. The query is an outgoing client call.
25
+ transaction.start_event(
26
+ :opentelemetry_kind => :client,
27
+ :opentelemetry_scope => ["appsignal-ruby/mongo", Appsignal::VERSION]
28
+ )
29
+ # Names the datastore this span talks to, which is what the trace
30
+ # timeline reads to recognize a database call, along with the command
31
+ # that ran, what it ran against, and the server it went to. Set here,
32
+ # where the event span it describes is the open one.
33
+ transaction.add_opentelemetry_attributes(
34
+ {
35
+ "db.system.name" => "mongodb",
36
+ "db.operation.name" => event.command_name,
37
+ "db.collection.name" => collection_name(event),
38
+ "db.namespace" => event.database_name,
39
+ "server.address" => event.address&.host,
40
+ "server.port" => event.address&.port
41
+ }.compact
42
+ )
24
43
  end
25
44
 
26
45
  # Called by Mongo::Monitor when query succeeds
@@ -46,9 +65,29 @@ module Appsignal
46
65
  store = transaction.store("mongo_driver")
47
66
  command = store.delete(event.request_id) || {}
48
67
 
68
+ # Say what kind of failure ended the query, which the OpenTelemetry
69
+ # semantic conventions ask for on a span whose operation failed. The
70
+ # driver reports a failure by calling us rather than by raising, so this
71
+ # is the only place it can be read. Set before the event is finished, so
72
+ # it lands on the query's own span.
73
+ #
74
+ # Only a failure event carries a failure, which is what tells the two
75
+ # apart here.
76
+ if event.respond_to?(:failure)
77
+ transaction.add_opentelemetry_attributes(
78
+ Appsignal::OpenTelemetry::ErrorType.attributes_for(error_name(event.failure))
79
+ )
80
+ # MongoDB's own code for the error, which the conventions ask for
81
+ # whenever the database reported one.
82
+ transaction.add_opentelemetry_attributes(
83
+ { "db.response.status_code" => error_code(event.failure) }.compact
84
+ )
85
+ end
86
+
49
87
  # Finish the event. The sanitized command is a (nested) Hash; emit it
50
- # as a JSON string. The agent serializes structured bodies to JSON
51
- # anyway, so this is equivalent output.
88
+ # as a JSON string so it works with both transaction backends. The
89
+ # agent serializes structured bodies to JSON anyway, so this is
90
+ # equivalent output there, and the collector receives a plain string.
52
91
  transaction.finish_event(
53
92
  "query.mongodb",
54
93
  "#{event.command_name} | #{event.database_name} | #{result}",
@@ -63,6 +102,36 @@ module Appsignal
63
102
  :database => event.database_name
64
103
  )
65
104
  end
105
+
106
+ private
107
+
108
+ # The collection a command worked on. MongoDB puts it in the field named
109
+ # after the command itself, as in `{ "find" => "users" }`. A command that
110
+ # works on the database as a whole rather than on one collection has
111
+ # something else in that field, such as the number 1, so only a String
112
+ # counts as a collection name.
113
+ def collection_name(event)
114
+ return unless event.command.respond_to?(:[])
115
+
116
+ collection = event.command[event.command_name]
117
+ collection if collection.is_a?(String)
118
+ end
119
+
120
+ # MongoDB's own code for an error, which it puts in the `code` field of the
121
+ # error document it replies with. Reported as a String, which is what the
122
+ # conventions ask for.
123
+ def error_code(failure)
124
+ return unless failure.respond_to?(:[])
125
+
126
+ failure["code"]&.to_s
127
+ end
128
+
129
+ # MongoDB names an error in the `codeName` field of the error document it
130
+ # replies with. A failure the driver never got a document for, such as a
131
+ # connection that dropped, has no name.
132
+ def error_name(failure)
133
+ failure["codeName"] if failure.respond_to?(:[])
134
+ end
66
135
  end
67
136
  end
68
137
  end
@@ -14,9 +14,38 @@ module Appsignal
14
14
 
15
15
  Appsignal.instrument(
16
16
  "request.net_http",
17
- "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}"
17
+ "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}",
18
+ :opentelemetry_kind => :client,
19
+ :opentelemetry_scope => ["appsignal-ruby/net_http", Appsignal::VERSION]
18
20
  ) do
19
- super
21
+ # Describes the span as an outgoing HTTP request. Together with the
22
+ # CLIENT kind, this is what the trace timeline reads to recognize it
23
+ # as one.
24
+ #
25
+ # The client's own `address` and `port` name the host being called.
26
+ # The request's `path` is a request target, so it can carry a query
27
+ # string, which the attribute builder cuts off.
28
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
29
+ Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
30
+ :method => request.method,
31
+ :scheme => use_ssl? ? "https" : "http",
32
+ :host => address,
33
+ :port => port,
34
+ :path => request.path
35
+ )
36
+ )
37
+ # Write trace context onto the outgoing request so the called service
38
+ # joins this trace. No-op outside collector mode. The request object
39
+ # is a valid carrier (it responds to `[]=`).
40
+ Appsignal::OpenTelemetry.inject_context(request)
41
+ # Describes the response on the same span, which the semantic
42
+ # conventions ask for whenever one was received. The event is still
43
+ # open here, so it lands on the request's own span.
44
+ super.tap do |response|
45
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
46
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(response&.code)
47
+ )
48
+ end
20
49
  end
21
50
  end
22
51
  end
@@ -13,7 +13,10 @@ module Appsignal
13
13
  end
14
14
 
15
15
  unless PumaServerHelper.ignored_error?(error)
16
- Appsignal.report_error(error) do |transaction|
16
+ Appsignal.report_error(
17
+ error,
18
+ :opentelemetry_scope => ["appsignal-ruby/puma", Appsignal::VERSION]
19
+ ) do |transaction|
17
20
  Appsignal::Rack::ApplyRackRequest
18
21
  .new(::Rack::Request.new(env))
19
22
  .apply_to(transaction)