appsignal 4.10.0-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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/Rakefile +87 -0
- data/appsignal.gemspec +8 -0
- data/build_matrix.yml +14 -1
- data/ext/appsignal_extension.c +14 -0
- data/ext/base.rb +8 -1
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/config.rb +140 -1
- data/lib/appsignal/demo.rb +1 -1
- data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
- data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
- data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
- data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
- data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
- data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
- data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
- data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
- data/lib/appsignal/event_formatter.rb +78 -0
- data/lib/appsignal/extension.rb +4 -0
- data/lib/appsignal/helpers/instrumentation.rb +236 -20
- data/lib/appsignal/helpers/metrics.rb +3 -24
- data/lib/appsignal/hooks/action_cable.rb +18 -6
- data/lib/appsignal/hooks/active_job.rb +128 -39
- data/lib/appsignal/hooks/at_exit.rb +4 -1
- data/lib/appsignal/hooks/excon.rb +20 -0
- data/lib/appsignal/hooks/faraday.rb +16 -0
- data/lib/appsignal/hooks/http.rb +5 -0
- data/lib/appsignal/hooks/resque.rb +1 -1
- data/lib/appsignal/hooks/sequel.rb +32 -2
- data/lib/appsignal/hooks/shoryuken.rb +3 -3
- data/lib/appsignal/hooks/sidekiq.rb +1 -1
- data/lib/appsignal/integrations/action_cable.rb +5 -2
- data/lib/appsignal/integrations/active_support_notifications.rb +59 -14
- data/lib/appsignal/integrations/data_mapper.rb +14 -2
- data/lib/appsignal/integrations/delayed_job_plugin.rb +45 -5
- data/lib/appsignal/integrations/dry_monitor.rb +39 -15
- data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
- data/lib/appsignal/integrations/excon.rb +52 -15
- data/lib/appsignal/integrations/faraday.rb +47 -12
- data/lib/appsignal/integrations/http.rb +43 -1
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
- data/lib/appsignal/integrations/net_http.rb +31 -2
- data/lib/appsignal/integrations/puma.rb +4 -1
- data/lib/appsignal/integrations/que.rb +224 -37
- data/lib/appsignal/integrations/railtie.rb +4 -1
- data/lib/appsignal/integrations/rake.rb +9 -3
- data/lib/appsignal/integrations/redis.rb +22 -1
- data/lib/appsignal/integrations/redis_client.rb +22 -1
- data/lib/appsignal/integrations/resque.rb +57 -10
- data/lib/appsignal/integrations/shoryuken.rb +140 -12
- data/lib/appsignal/integrations/sidekiq.rb +73 -16
- data/lib/appsignal/integrations/webmachine.rb +47 -4
- data/lib/appsignal/loaders/padrino.rb +2 -1
- data/lib/appsignal/logger/extension_backend.rb +24 -0
- data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
- data/lib/appsignal/logger.rb +13 -9
- data/lib/appsignal/metrics/extension_backend.rb +47 -0
- data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
- data/lib/appsignal/opentelemetry/attributes.rb +31 -0
- data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
- data/lib/appsignal/opentelemetry/error_type.rb +37 -0
- data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
- data/lib/appsignal/opentelemetry/http_method.rb +59 -0
- data/lib/appsignal/opentelemetry/http_response.rb +30 -0
- data/lib/appsignal/opentelemetry/http_server_request.rb +45 -0
- data/lib/appsignal/opentelemetry/messaging.rb +82 -0
- data/lib/appsignal/opentelemetry/rendering.rb +29 -0
- data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
- data/lib/appsignal/opentelemetry.rb +337 -0
- data/lib/appsignal/rack/abstract_middleware.rb +63 -4
- data/lib/appsignal/rack/body_wrapper.rb +18 -5
- data/lib/appsignal/rack/event_handler.rb +36 -3
- data/lib/appsignal/rack/grape_middleware.rb +1 -0
- data/lib/appsignal/rack/hanami_middleware.rb +2 -1
- data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
- data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
- data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
- data/lib/appsignal/rack.rb +39 -11
- data/lib/appsignal/sample_data.rb +4 -0
- data/lib/appsignal/transaction/base_backend.rb +107 -0
- data/lib/appsignal/transaction/extension_backend.rb +203 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +796 -0
- data/lib/appsignal/transaction.rb +537 -144
- data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +9 -0
- data/sig/appsignal.rbi +426 -37
- data/sig/appsignal.rbs +386 -27
- metadata +23 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "appsignal/opentelemetry/attributes"
|
|
4
|
+
|
|
5
|
+
module Appsignal
|
|
6
|
+
module Metrics
|
|
7
|
+
# @!visibility private
|
|
8
|
+
#
|
|
9
|
+
# Routes custom metric helper calls through the OpenTelemetry metrics
|
|
10
|
+
# SDK using the meter provider configured at `Appsignal.start` time when
|
|
11
|
+
# collector mode is active. Mirrors the Python integration's
|
|
12
|
+
# `appsignal/metrics.py`:
|
|
13
|
+
#
|
|
14
|
+
# - `set_gauge` uses a synchronous OTel Gauge.
|
|
15
|
+
# - `increment_counter` uses an UpDownCounter so negative increments
|
|
16
|
+
# work (Counter would reject them).
|
|
17
|
+
# - `add_distribution_value` uses a Histogram.
|
|
18
|
+
#
|
|
19
|
+
# Instruments are created once per name and cached: the OTel SDK logs a
|
|
20
|
+
# "duplicate instrument registration" warning and swaps the instrument
|
|
21
|
+
# if `create_*` is called again for the same name. Tags attach at record
|
|
22
|
+
# time, not at instrument creation time.
|
|
23
|
+
module OpenTelemetryBackend
|
|
24
|
+
MUTEX = Mutex.new
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def set_gauge(name, value, tags)
|
|
28
|
+
instrument(:gauge, name).record(
|
|
29
|
+
value.to_f,
|
|
30
|
+
:attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def increment_counter(name, value, tags)
|
|
35
|
+
instrument(:up_down_counter, name).add(
|
|
36
|
+
value.to_f,
|
|
37
|
+
:attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_distribution_value(name, value, tags)
|
|
42
|
+
instrument(:histogram, name).record(
|
|
43
|
+
value.to_f,
|
|
44
|
+
:attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @!visibility private
|
|
49
|
+
#
|
|
50
|
+
# Test-only. Drops the cached meter and instruments so the next
|
|
51
|
+
# call re-resolves `OpenTelemetry.meter_provider`.
|
|
52
|
+
def reset!
|
|
53
|
+
MUTEX.synchronize do
|
|
54
|
+
@meter = nil
|
|
55
|
+
@gauges = nil
|
|
56
|
+
@counters = nil
|
|
57
|
+
@histograms = nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Fetch the named instrument, creating and caching it on first use.
|
|
64
|
+
# The lookup-or-create runs under the mutex so two concurrent
|
|
65
|
+
# first-time calls don't both create the instrument (which would
|
|
66
|
+
# make the SDK log a duplicate-registration warning).
|
|
67
|
+
def instrument(kind, name)
|
|
68
|
+
name = name.to_s
|
|
69
|
+
MUTEX.synchronize do
|
|
70
|
+
case kind
|
|
71
|
+
when :gauge
|
|
72
|
+
(@gauges ||= {})[name] ||= meter.create_gauge(name)
|
|
73
|
+
when :up_down_counter
|
|
74
|
+
(@counters ||= {})[name] ||= meter.create_up_down_counter(name)
|
|
75
|
+
when :histogram
|
|
76
|
+
(@histograms ||= {})[name] ||= meter.create_histogram(name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Only called from `instrument` while the mutex is held, so the plain
|
|
82
|
+
# memoisation needs no extra locking of its own.
|
|
83
|
+
def meter
|
|
84
|
+
@meter ||= ::OpenTelemetry.meter_provider.meter("appsignal-helpers")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Coerces user-supplied tag hashes into a shape the OpenTelemetry SDK
|
|
8
|
+
# accepts as attribute values: string keys, and values restricted to
|
|
9
|
+
# the primitive types the OTLP spec allows. Anything else falls back
|
|
10
|
+
# to `to_s`. Shared by the metric and log backends so both behave
|
|
11
|
+
# identically.
|
|
12
|
+
module Attributes
|
|
13
|
+
class << self
|
|
14
|
+
def format(attrs)
|
|
15
|
+
attrs.each_with_object({}) do |(key, value), result|
|
|
16
|
+
result[key.to_s] = format_value(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def format_value(value)
|
|
23
|
+
case value
|
|
24
|
+
when String, Integer, Float, TrueClass, FalseClass then value
|
|
25
|
+
else value.to_s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# The OpenTelemetry gems collector mode supports, mapped to their version
|
|
8
|
+
# requirements. Each gem maps to an array of requirement strings so a gem
|
|
9
|
+
# can carry more than one constraint later, even though each holds a single
|
|
10
|
+
# `~>` requirement today. These gems are *not* declared in the gemspec:
|
|
11
|
+
# they are optional and only required when collector mode is active. Apps
|
|
12
|
+
# that opt into collector mode install them into their own bundle (see the
|
|
13
|
+
# collector documentation).
|
|
14
|
+
#
|
|
15
|
+
# Each requirement pins a floor and a ceiling. The floor is the version the
|
|
16
|
+
# minimum supported Ruby (3.1) resolves to in our CI collector matrix. The
|
|
17
|
+
# ceiling is a pessimistic `~>` cap at the next major version. We set the
|
|
18
|
+
# cap loosely on purpose so it does not block customers from updating these
|
|
19
|
+
# gems within a major version.
|
|
20
|
+
#
|
|
21
|
+
# This file must stay free of any other dependency so it can be required
|
|
22
|
+
# directly from a Gemfile (see `gemfiles/collector.rb`) and from the
|
|
23
|
+
# runtime version gate in `Appsignal::OpenTelemetry.configure` without
|
|
24
|
+
# loading the rest of the gem.
|
|
25
|
+
REQUIRED_GEMS = {
|
|
26
|
+
"opentelemetry-sdk" => ["~> 1.10"],
|
|
27
|
+
"opentelemetry-common" => ["~> 0.23"],
|
|
28
|
+
"opentelemetry-metrics-sdk" => ["~> 0.12"],
|
|
29
|
+
"opentelemetry-logs-sdk" => ["~> 0.4"],
|
|
30
|
+
"opentelemetry-exporter-otlp" => ["~> 0.32"],
|
|
31
|
+
"opentelemetry-exporter-otlp-metrics" => ["~> 0.7"],
|
|
32
|
+
"opentelemetry-exporter-otlp-logs" => ["~> 0.3"]
|
|
33
|
+
}.freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attribute that describes what kind of failure
|
|
8
|
+
# ended an operation.
|
|
9
|
+
#
|
|
10
|
+
# The semantic conventions ask for `error.type` on a span whose operation
|
|
11
|
+
# failed, and for it to be left unset on one that succeeded. The value has to
|
|
12
|
+
# have low cardinality, which for an exception means its class name rather
|
|
13
|
+
# than its message. A failure we have no name for becomes `_OTHER`, which is
|
|
14
|
+
# the fallback the conventions define.
|
|
15
|
+
module ErrorType
|
|
16
|
+
ATTRIBUTE = "error.type"
|
|
17
|
+
|
|
18
|
+
# The value the semantic conventions use for a failure the
|
|
19
|
+
# instrumentation has no name for.
|
|
20
|
+
OTHER = "_OTHER"
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# The attributes describing the given failure, as a Hash to pass to
|
|
24
|
+
# `add_opentelemetry_attributes`. Takes the name of the failure: an
|
|
25
|
+
# exception's class name, or the error code a datastore reported.
|
|
26
|
+
#
|
|
27
|
+
# An anonymous exception class has no name, and a datastore does not
|
|
28
|
+
# always report a code, so a missing name falls back to `_OTHER` rather
|
|
29
|
+
# than leaving the span with no `error.type` at all.
|
|
30
|
+
def attributes_for(name)
|
|
31
|
+
value = name.to_s
|
|
32
|
+
{ ATTRIBUTE => value.empty? ? OTHER : value }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe an outgoing HTTP request.
|
|
8
|
+
#
|
|
9
|
+
# The semantic conventions ask for the request method, the host and port
|
|
10
|
+
# being called, and the full URL. Those together are also what the trace
|
|
11
|
+
# timeline reads to recognize an outgoing request.
|
|
12
|
+
#
|
|
13
|
+
# The URL is built from the parts rather than taken whole, which has two
|
|
14
|
+
# consequences worth knowing about.
|
|
15
|
+
#
|
|
16
|
+
# It never contains credentials. A URL can carry a username and password,
|
|
17
|
+
# which the conventions say must not be sent. Building the URL from the
|
|
18
|
+
# scheme, host, port and path means there is nowhere for them to come from.
|
|
19
|
+
#
|
|
20
|
+
# It never contains the query string either. The conventions do ask for it,
|
|
21
|
+
# but the query string of a call to somebody else's API is the part most
|
|
22
|
+
# likely to carry a key or a token, and unlike an incoming request it is not
|
|
23
|
+
# something the application chose the shape of. Anything a caller passes as a
|
|
24
|
+
# path is cut at the first question mark for the same reason, because some
|
|
25
|
+
# clients hand us a request target rather than a path.
|
|
26
|
+
module HttpClientRequest
|
|
27
|
+
ADDRESS_ATTRIBUTE = "server.address"
|
|
28
|
+
PORT_ATTRIBUTE = "server.port"
|
|
29
|
+
URL_ATTRIBUTE = "url.full"
|
|
30
|
+
|
|
31
|
+
# The port each scheme uses when a URL does not name one. Left out of the
|
|
32
|
+
# URL, which is how a URL is normally written, but still reported as the
|
|
33
|
+
# port, which the conventions ask for either way.
|
|
34
|
+
DEFAULT_PORTS = {
|
|
35
|
+
"http" => 80,
|
|
36
|
+
"https" => 443
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# The attributes describing the given request, as a Hash to pass to
|
|
41
|
+
# `add_opentelemetry_attributes`. Takes the request's parts, because
|
|
42
|
+
# some clients hand us a URI and others only the parts.
|
|
43
|
+
def attributes_for(method:, scheme: nil, host: nil, port: nil, path: nil)
|
|
44
|
+
attributes = HttpMethod.attributes_for(method)
|
|
45
|
+
attributes[ADDRESS_ATTRIBUTE] = host.to_s unless host.to_s.empty?
|
|
46
|
+
|
|
47
|
+
port = port_for(scheme, port)
|
|
48
|
+
attributes[PORT_ATTRIBUTE] = port if port
|
|
49
|
+
|
|
50
|
+
url = url_for(scheme, host, port, path)
|
|
51
|
+
attributes[URL_ATTRIBUTE] = url if url
|
|
52
|
+
|
|
53
|
+
attributes
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# A client that was not given a port uses the one its scheme implies, so
|
|
59
|
+
# report that rather than nothing.
|
|
60
|
+
def port_for(scheme, port)
|
|
61
|
+
Integer(port, :exception => false) || DEFAULT_PORTS[scheme.to_s]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def url_for(scheme, host, port, path)
|
|
65
|
+
return if scheme.to_s.empty? || host.to_s.empty?
|
|
66
|
+
|
|
67
|
+
url = +"#{scheme}://#{host}"
|
|
68
|
+
url << ":#{port}" if port && port != DEFAULT_PORTS[scheme.to_s]
|
|
69
|
+
url << path_without_query(path)
|
|
70
|
+
url
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Some clients report the path of a request to the root of a host as
|
|
74
|
+
# empty, and others as "/". The request goes to "/" either way, so say so
|
|
75
|
+
# rather than let the URL differ by which client made the request.
|
|
76
|
+
def path_without_query(path)
|
|
77
|
+
without_query = path.to_s.split("?", 2).first.to_s
|
|
78
|
+
without_query.empty? ? "/" : without_query
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe an HTTP request method.
|
|
8
|
+
#
|
|
9
|
+
# The semantic conventions treat `http.request.method` as a closed set of
|
|
10
|
+
# known method names, so an arbitrary value cannot be passed through. A
|
|
11
|
+
# method outside the set becomes `_OTHER`, and a method that only matches
|
|
12
|
+
# after upcasing is replaced by its canonical form. Both of those cases keep
|
|
13
|
+
# the value we were given in `http.request.method_original`, so the original
|
|
14
|
+
# is never lost.
|
|
15
|
+
module HttpMethod
|
|
16
|
+
# The methods the semantic conventions know about: those defined in
|
|
17
|
+
# RFC 9110, plus PATCH from RFC 5789.
|
|
18
|
+
KNOWN_METHODS = [
|
|
19
|
+
"CONNECT",
|
|
20
|
+
"DELETE",
|
|
21
|
+
"GET",
|
|
22
|
+
"HEAD",
|
|
23
|
+
"OPTIONS",
|
|
24
|
+
"PATCH",
|
|
25
|
+
"POST",
|
|
26
|
+
"PUT",
|
|
27
|
+
"TRACE"
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# The value the semantic conventions use for a method they do not know.
|
|
31
|
+
OTHER = "_OTHER"
|
|
32
|
+
|
|
33
|
+
METHOD_ATTRIBUTE = "http.request.method"
|
|
34
|
+
ORIGINAL_ATTRIBUTE = "http.request.method_original"
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# The attributes describing the given request method, as a Hash to pass
|
|
38
|
+
# to `add_opentelemetry_attributes`. Returns an empty Hash when there is
|
|
39
|
+
# no method to describe, so a caller that could not read one can pass
|
|
40
|
+
# the result on without checking.
|
|
41
|
+
def attributes_for(method)
|
|
42
|
+
original = method.to_s
|
|
43
|
+
return {} if original.empty?
|
|
44
|
+
|
|
45
|
+
# Method names are case sensitive, so a value that already matches a
|
|
46
|
+
# known method is used as it is, with nothing to preserve.
|
|
47
|
+
return { METHOD_ATTRIBUTE => original } if KNOWN_METHODS.include?(original)
|
|
48
|
+
|
|
49
|
+
canonical = original.upcase
|
|
50
|
+
if KNOWN_METHODS.include?(canonical)
|
|
51
|
+
{ METHOD_ATTRIBUTE => canonical, ORIGINAL_ATTRIBUTE => original }
|
|
52
|
+
else
|
|
53
|
+
{ METHOD_ATTRIBUTE => OTHER, ORIGINAL_ATTRIBUTE => original }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe an HTTP response.
|
|
8
|
+
#
|
|
9
|
+
# The semantic conventions ask for the response status code on the span of an
|
|
10
|
+
# HTTP request, whether that is a request this application handled or one it
|
|
11
|
+
# made. They ask for it only when a response was actually received or sent,
|
|
12
|
+
# so a request that never got one is described without it.
|
|
13
|
+
module HttpResponse
|
|
14
|
+
STATUS_CODE_ATTRIBUTE = "http.response.status_code"
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# The attributes describing the given response status, as a Hash to pass
|
|
18
|
+
# to `add_opentelemetry_attributes`. Returns an empty Hash when there is
|
|
19
|
+
# no status to describe, so a caller whose request produced no response
|
|
20
|
+
# can pass the result on without checking.
|
|
21
|
+
def attributes_for(status)
|
|
22
|
+
code = Integer(status, :exception => false)
|
|
23
|
+
return {} unless code
|
|
24
|
+
|
|
25
|
+
{ STATUS_CODE_ATTRIBUTE => code }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe an incoming HTTP
|
|
8
|
+
# request.
|
|
9
|
+
#
|
|
10
|
+
# The semantic conventions ask for the request method, the path and the
|
|
11
|
+
# scheme on the span of a request a server handled. Those three together are
|
|
12
|
+
# also what the trace timeline reads to recognize a web request.
|
|
13
|
+
#
|
|
14
|
+
# The path is the concrete path the request was made to, such as `/users/1`.
|
|
15
|
+
# It is not the route template the application matched it against, which the
|
|
16
|
+
# conventions call `http.route` and which a Rack application does not
|
|
17
|
+
# necessarily have.
|
|
18
|
+
#
|
|
19
|
+
# The query string is asked for whenever the request had one. It is sent
|
|
20
|
+
# whole, without the leading question mark, and it is not filtered here. The
|
|
21
|
+
# collector filters it with the `filter_request_query_parameters` option, and
|
|
22
|
+
# builds the request's query parameters out of it.
|
|
23
|
+
#
|
|
24
|
+
# Every value is optional, because reading any of them from the request can
|
|
25
|
+
# fail. An attribute we have no value for is left out rather than sent
|
|
26
|
+
# empty.
|
|
27
|
+
module HttpServerRequest
|
|
28
|
+
PATH_ATTRIBUTE = "url.path"
|
|
29
|
+
SCHEME_ATTRIBUTE = "url.scheme"
|
|
30
|
+
QUERY_ATTRIBUTE = "url.query"
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# The attributes describing the given request, as a Hash to pass to
|
|
34
|
+
# `add_opentelemetry_attributes`.
|
|
35
|
+
def attributes_for(method:, path: nil, scheme: nil, query: nil)
|
|
36
|
+
attributes = HttpMethod.attributes_for(method)
|
|
37
|
+
attributes[PATH_ATTRIBUTE] = path.to_s unless path.to_s.empty?
|
|
38
|
+
attributes[SCHEME_ATTRIBUTE] = scheme.to_s unless scheme.to_s.empty?
|
|
39
|
+
attributes[QUERY_ATTRIBUTE] = query.to_s unless query.to_s.empty?
|
|
40
|
+
attributes
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe a messaging operation,
|
|
8
|
+
# which for AppSignal means a background job being enqueued or performed.
|
|
9
|
+
#
|
|
10
|
+
# The semantic conventions ask for the messaging system, the name of the
|
|
11
|
+
# operation, and the kind of operation it was. The name is the job library's
|
|
12
|
+
# own word for what happened. The kind is one of the five values the
|
|
13
|
+
# conventions define, and AppSignal only ever produces two of them:
|
|
14
|
+
# enqueuing a job sends a message, and performing one processes it.
|
|
15
|
+
#
|
|
16
|
+
# They also ask for the destination of the message, which for a job library
|
|
17
|
+
# is the queue it is on, and for how many messages a span covers when it
|
|
18
|
+
# covers a batch of them.
|
|
19
|
+
#
|
|
20
|
+
# The system is the only part that differs per job library, so each
|
|
21
|
+
# integration passes its own.
|
|
22
|
+
module Messaging
|
|
23
|
+
SYSTEM_ATTRIBUTE = "messaging.system"
|
|
24
|
+
OPERATION_NAME_ATTRIBUTE = "messaging.operation.name"
|
|
25
|
+
OPERATION_TYPE_ATTRIBUTE = "messaging.operation.type"
|
|
26
|
+
DESTINATION_ATTRIBUTE = "messaging.destination.name"
|
|
27
|
+
BATCH_COUNT_ATTRIBUTE = "messaging.batch.message_count"
|
|
28
|
+
|
|
29
|
+
# The operations AppSignal records, and the kind of operation the
|
|
30
|
+
# conventions class each of them as.
|
|
31
|
+
ENQUEUE = "enqueue"
|
|
32
|
+
PERFORM = "perform"
|
|
33
|
+
OPERATION_TYPES = {
|
|
34
|
+
ENQUEUE => "send",
|
|
35
|
+
PERFORM => "process"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# The attributes describing a job being enqueued, as a Hash to pass to
|
|
40
|
+
# `add_opentelemetry_attributes`.
|
|
41
|
+
def enqueue_attributes(system, destination: nil, batch_size: nil)
|
|
42
|
+
attributes_for(system, ENQUEUE, destination, batch_size)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The attributes describing a job being performed, as a Hash to pass to
|
|
46
|
+
# `add_opentelemetry_attributes`.
|
|
47
|
+
def perform_attributes(system, destination: nil, batch_size: nil)
|
|
48
|
+
attributes_for(system, PERFORM, destination, batch_size)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def attributes_for(system, operation, destination, batch_size)
|
|
54
|
+
{
|
|
55
|
+
SYSTEM_ATTRIBUTE => system,
|
|
56
|
+
OPERATION_NAME_ATTRIBUTE => operation,
|
|
57
|
+
OPERATION_TYPE_ATTRIBUTE => OPERATION_TYPES.fetch(operation),
|
|
58
|
+
DESTINATION_ATTRIBUTE => destination_name(destination),
|
|
59
|
+
BATCH_COUNT_ATTRIBUTE => batch_count(batch_size)
|
|
60
|
+
}.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The queue the job is on, which the conventions call the destination of
|
|
64
|
+
# the message. A job whose queue we cannot name is described without it.
|
|
65
|
+
def destination_name(destination)
|
|
66
|
+
name = destination.to_s
|
|
67
|
+
name unless name.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# How many messages the span covers. The conventions ask for this only
|
|
71
|
+
# when a span describes a batch, and say it must not be set on a span
|
|
72
|
+
# that describes a single message.
|
|
73
|
+
def batch_count(batch_size)
|
|
74
|
+
return unless batch_size
|
|
75
|
+
|
|
76
|
+
count = batch_size.to_i
|
|
77
|
+
count if count.positive?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Builds the OpenTelemetry attributes that describe an event as template
|
|
8
|
+
# rendering.
|
|
9
|
+
#
|
|
10
|
+
# Rendering a template has no semantic convention to describe it, so these
|
|
11
|
+
# events say which group they belong to directly. The trace timeline reads
|
|
12
|
+
# `appsignal.group` before it looks at any convention attribute, and
|
|
13
|
+
# "render" is the group it shows as "Templating".
|
|
14
|
+
module Rendering
|
|
15
|
+
GROUP_ATTRIBUTE = "appsignal.group"
|
|
16
|
+
GROUP = "render"
|
|
17
|
+
|
|
18
|
+
ATTRIBUTES = { GROUP_ATTRIBUTE => GROUP }.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# The attributes describing an event as template rendering, as a Hash
|
|
22
|
+
# to pass to `add_opentelemetry_attributes`.
|
|
23
|
+
def attributes
|
|
24
|
+
ATTRIBUTES
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Maps a SQL library's own name for the engine it is talking to onto the
|
|
8
|
+
# `db.system.name` semantic conventions value for that engine.
|
|
9
|
+
#
|
|
10
|
+
# Each library names engines differently, and none of them matches the
|
|
11
|
+
# semantic conventions spelling, so every SQL integration needs a lookup.
|
|
12
|
+
# Kept as one map per library, each keyed on that library's own
|
|
13
|
+
# vocabulary, rather than one shared map: ActiveRecord's `ADAPTER_NAME`
|
|
14
|
+
# strings, Sequel's `database_type` symbols, and DataMapper's
|
|
15
|
+
# `DataObjects` connection class names do not collide today, but nothing
|
|
16
|
+
# about that is enforced by combining them. A wrong value passed into the
|
|
17
|
+
# wrong library's lookup would silently return another library's answer
|
|
18
|
+
# instead of nil, and a library's own gaps would be invisible next to two
|
|
19
|
+
# other libraries' complete entries. ROM is the exception: its payload
|
|
20
|
+
# already carries Sequel's own `database_type` symbol, so it is
|
|
21
|
+
# legitimate for it to share Sequel's map, not just convenient.
|
|
22
|
+
#
|
|
23
|
+
# A name none of these maps recognise returns `nil`, so the caller's own
|
|
24
|
+
# `other_sql` fallback applies -- exactly what already happens for a
|
|
25
|
+
# library this gem has no mapping for at all.
|
|
26
|
+
#
|
|
27
|
+
# This intentionally follows the older semantic conventions value set:
|
|
28
|
+
# SQL Server maps to `mssql`, not the current registry's
|
|
29
|
+
# `microsoft.sql_server`, and Oracle maps to `oracle`, not `oracle.db`.
|
|
30
|
+
# That is what the AppSignal collector's sanitizer recognizes today;
|
|
31
|
+
# emitting the newer values would silently turn sanitization off for
|
|
32
|
+
# those engines' queries.
|
|
33
|
+
module SqlDbSystem
|
|
34
|
+
# ActiveRecord's `ADAPTER_NAME`. Rails bundles the Postgres, Mysql2,
|
|
35
|
+
# SQLite and (7.1+) Trilogy adapters; SQL Server and Oracle come from
|
|
36
|
+
# the separate `activerecord-sqlserver-adapter` and
|
|
37
|
+
# `activerecord-oracle_enhanced-adapter` gems, which declare
|
|
38
|
+
# `ADAPTER_NAME` the same way.
|
|
39
|
+
ACTIVE_RECORD = {
|
|
40
|
+
"PostgreSQL" => "postgresql",
|
|
41
|
+
"Mysql2" => "mysql",
|
|
42
|
+
"Trilogy" => "mysql",
|
|
43
|
+
"SQLite" => "sqlite",
|
|
44
|
+
"SQLServer" => "mssql",
|
|
45
|
+
"OracleEnhanced" => "oracle"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Sequel's `database_type`. ROM's dry-monitor payload reuses this
|
|
49
|
+
# symbol directly, so `name_for_sequel` is also ROM's lookup.
|
|
50
|
+
SEQUEL = {
|
|
51
|
+
:postgres => "postgresql",
|
|
52
|
+
:mysql => "mysql",
|
|
53
|
+
:sqlite => "sqlite",
|
|
54
|
+
:mssql => "mssql",
|
|
55
|
+
:oracle => "oracle"
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
# DataMapper's `DataObjects` connection classes.
|
|
59
|
+
DATA_MAPPER = {
|
|
60
|
+
"DataObjects::Postgres::Connection" => "postgresql",
|
|
61
|
+
"DataObjects::Mysql::Connection" => "mysql",
|
|
62
|
+
"DataObjects::Sqlite3::Connection" => "sqlite",
|
|
63
|
+
"DataObjects::SqlServer::Connection" => "mssql"
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# The `db.system.name` value for an ActiveRecord connection's
|
|
68
|
+
# `adapter_name`, or `nil` when the adapter is not one this map
|
|
69
|
+
# recognises.
|
|
70
|
+
def name_for_active_record(adapter_name)
|
|
71
|
+
ACTIVE_RECORD[adapter_name]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The `db.system.name` value for Sequel's `database_type`, or `nil`
|
|
75
|
+
# when it is not one this map recognises. Also the lookup ROM's
|
|
76
|
+
# formatter uses, since ROM reports this same symbol.
|
|
77
|
+
def name_for_sequel(database_type)
|
|
78
|
+
SEQUEL[database_type]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The `db.system.name` value for DataMapper's connection class name,
|
|
82
|
+
# or `nil` when it is not one this map recognises.
|
|
83
|
+
def name_for_data_mapper(connection_class_name)
|
|
84
|
+
DATA_MAPPER[connection_class_name]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|