rails-profiler 0.39.0 → 0.40.0
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 +8 -0
- data/lib/profiler/middleware/profiler_middleware.rb +3 -1
- data/lib/profiler/otel/metrics_exporter.rb +135 -0
- data/lib/profiler/otel/sdk.rb +104 -50
- data/lib/profiler/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 312d0ee269f45025f4192fd8e7491d90835e4d2da0a9ddfdbda6429644615981
|
|
4
|
+
data.tar.gz: 4dd491cba56414e4ed0291ff492e644cd6264a27e6830da54f23caebfcb9d666
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20de3f4b06ce2e7827a12fc8d5304428cc7865414b820177001957c155e4e18afa0bbd8fb0acef04d624ee609cc9473d1cf2eb456699b4bdbfdc241b8ff06ec2
|
|
7
|
+
data.tar.gz: beea47dda7f64599af19dd12c44531cdc9c9310c6069782754a2295ff2df5a11aedee1cc1f14c407b2be2f38c8d89b6aedc228405d74eddaf0ead39a08f704e3
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,14 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.40.0] - 2026-10-19
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **OpenTelemetry:** with the same `config.otel_enabled = true`, each profiled request also records metrics: duration histograms (request, SQL query, view, cache operation, outbound `Net::HTTP` call) and counters for allocated objects and for requests that ran an N+1. Attributes are low-cardinality on purpose: `controller#action`, HTTP method and status, SQL verb and cache operation, never a token, URL or SQL text. **Experimental:** it needs `opentelemetry-metrics-api`, `opentelemetry-metrics-sdk` and `opentelemetry-exporter-otlp-metrics` in the application's `Gemfile`, pinned to an exact version because the metrics SDK is alpha; without them one message is logged and traces are unaffected.
|
|
29
|
+
|
|
22
30
|
## [0.39.0] - 2026-10-19
|
|
23
31
|
|
|
24
32
|
<!-- stamped -->
|
|
@@ -5,6 +5,7 @@ require_relative "../models/profile"
|
|
|
5
5
|
require_relative "../current_context"
|
|
6
6
|
require_relative "../trace_context"
|
|
7
7
|
require_relative "../otel/trace_exporter"
|
|
8
|
+
require_relative "../otel/metrics_exporter"
|
|
8
9
|
require_relative "../collectors/lifecycle"
|
|
9
10
|
require_relative "toolbar_injector"
|
|
10
11
|
require_relative "capturing_body"
|
|
@@ -223,12 +224,13 @@ module Profiler
|
|
|
223
224
|
Profiler.log_error("ProfilerMiddleware: could not finish the profile of a streamed response", e)
|
|
224
225
|
end
|
|
225
226
|
|
|
226
|
-
# The profile is saved, then sent to OpenTelemetry when config.otel_enabled asks for it:
|
|
227
|
+
# The profile is saved, then sent to OpenTelemetry (spans, then metrics) when config.otel_enabled asks for it:
|
|
227
228
|
# once per profile, here, and not from Profiler.save_profile, which a profile reaches again
|
|
228
229
|
# whenever a late outbound call completes. The export never raises.
|
|
229
230
|
def save_and_export(profile)
|
|
230
231
|
saved = Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
231
232
|
Otel::TraceExporter.export(profile)
|
|
233
|
+
Otel::MetricsExporter.export(profile)
|
|
232
234
|
saved
|
|
233
235
|
end
|
|
234
236
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sdk"
|
|
4
|
+
require_relative "trace_exporter"
|
|
5
|
+
require_relative "../mcp/resources/n1_patterns"
|
|
6
|
+
|
|
7
|
+
module Profiler
|
|
8
|
+
module Otel
|
|
9
|
+
# Records a finished request profile in OpenTelemetry metrics: duration histograms (request,
|
|
10
|
+
# SQL query, view or partial, cache operation, outbound HTTP call; milliseconds, the unit the
|
|
11
|
+
# profiler measures in), a counter of the objects the request allocated, and a counter of the
|
|
12
|
+
# requests that ran an N+1. The events are the ones TraceExporter turns into spans, here
|
|
13
|
+
# aggregated.
|
|
14
|
+
#
|
|
15
|
+
# Attributes are kept to a bounded set of values, on purpose (unlike spans, a metric is one
|
|
16
|
+
# time series per attribute combination): controller#action (or "unknown"), the HTTP method
|
|
17
|
+
# (a standard verb, else "_OTHER"), the status code (request duration only), the SQL verb
|
|
18
|
+
# (SELECT, INSERT, UPDATE, DELETE, else "OTHER") and the cache operation. Never a profile
|
|
19
|
+
# token, a path or URL, SQL text, a cache key or a host.
|
|
20
|
+
#
|
|
21
|
+
# The metrics SDK is alpha: its API moves between releases (see docs/configuration.md).
|
|
22
|
+
# Does nothing, and touches nothing of OpenTelemetry, unless config.otel_enabled is true.
|
|
23
|
+
# Never raises: a failure is logged and the request goes on.
|
|
24
|
+
module MetricsExporter
|
|
25
|
+
SCOPE_NAME = "profiler"
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
HTTP_METHODS = %w[GET HEAD POST PUT PATCH DELETE OPTIONS CONNECT TRACE].freeze
|
|
28
|
+
SQL_VERBS = %w[SELECT INSERT UPDATE DELETE].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def export(profile)
|
|
33
|
+
return false unless Profiler.configuration.otel_enabled
|
|
34
|
+
|
|
35
|
+
provider = Sdk.meter_provider
|
|
36
|
+
return false unless provider
|
|
37
|
+
|
|
38
|
+
record(instruments(provider), profile)
|
|
39
|
+
true
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
Profiler.log_error_once(:otel_metrics_export, "OpenTelemetry metrics export of a profile failed", e)
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The instruments of a provider (the meter caches them by name, so asking again is cheap).
|
|
46
|
+
def instruments(provider)
|
|
47
|
+
meter = provider.meter(SCOPE_NAME, version: Profiler.version)
|
|
48
|
+
{
|
|
49
|
+
request: meter.create_histogram("profiler.request.duration", unit: "ms",
|
|
50
|
+
description: "Duration of a profiled HTTP request"),
|
|
51
|
+
sql: meter.create_histogram("profiler.db.query.duration", unit: "ms",
|
|
52
|
+
description: "Duration of a SQL query"),
|
|
53
|
+
view: meter.create_histogram("profiler.view.render.duration", unit: "ms",
|
|
54
|
+
description: "Duration of a view or partial render"),
|
|
55
|
+
cache: meter.create_histogram("profiler.cache.operation.duration", unit: "ms",
|
|
56
|
+
description: "Duration of a cache operation"),
|
|
57
|
+
http: meter.create_histogram("profiler.http.client.request.duration", unit: "ms",
|
|
58
|
+
description: "Duration of an outbound Net::HTTP call"),
|
|
59
|
+
allocations: meter.create_counter("profiler.request.allocations", unit: "{object}",
|
|
60
|
+
description: "Objects allocated by profiled requests"),
|
|
61
|
+
n_plus_one: meter.create_counter("profiler.request.n_plus_one", unit: "{request}",
|
|
62
|
+
description: "Profiled requests that ran an N+1 query")
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def record(instruments, profile)
|
|
67
|
+
data = profile.collectors_data || {}
|
|
68
|
+
base = { "profiler.controller_action" => controller_action(data), "http.request.method" => method_name(profile.method) }
|
|
69
|
+
|
|
70
|
+
request_attributes = base.dup
|
|
71
|
+
request_attributes["http.response.status_code"] = profile.status.to_i if (100..599).cover?(profile.status.to_i)
|
|
72
|
+
instruments[:request].record(profile.duration.to_f, attributes: request_attributes) if profile.duration
|
|
73
|
+
|
|
74
|
+
queries = queries(data)
|
|
75
|
+
queries.each do |query|
|
|
76
|
+
instruments[:sql].record(query[:duration].to_f, attributes: base.merge("db.operation.name" => sql_verb(query[:sql])))
|
|
77
|
+
end
|
|
78
|
+
record_all(instruments[:view], base, views(data))
|
|
79
|
+
record_cache(instruments[:cache], base, data)
|
|
80
|
+
record_all(instruments[:http], base, TraceExporter.list(TraceExporter.fetch(data, "http"), :requests).reject { |r| r[:in_flight] })
|
|
81
|
+
|
|
82
|
+
allocations = profile.allocated_objects
|
|
83
|
+
instruments[:allocations].add(allocations, attributes: base) if allocations.is_a?(Integer) && allocations.positive?
|
|
84
|
+
instruments[:n_plus_one].add(1, attributes: base) if n_plus_one?(queries)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def record_all(histogram, attributes, events)
|
|
88
|
+
events.each { |event| histogram.record(event[:duration].to_f, attributes: attributes) if event[:duration] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def record_cache(histogram, base, data)
|
|
92
|
+
cache = TraceExporter.fetch(data, "cache")
|
|
93
|
+
%i[reads writes deletes].each do |kind|
|
|
94
|
+
operation = kind.to_s.chomp("s")
|
|
95
|
+
TraceExporter.list(cache, kind).each do |event|
|
|
96
|
+
next unless event[:duration]
|
|
97
|
+
|
|
98
|
+
histogram.record(event[:duration].to_f, attributes: base.merge("cache.operation" => operation))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def views(data)
|
|
104
|
+
view = TraceExporter.fetch(data, "view")
|
|
105
|
+
TraceExporter.list(view, :views) + TraceExporter.list(view, :partials)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The SQL queries of the request, transaction statements apart (the Database tab does too).
|
|
109
|
+
def queries(data)
|
|
110
|
+
TraceExporter.list(TraceExporter.fetch(data, "database"), :queries).reject { |q| q[:transaction] || q[:duration].nil? }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# An N+1 here: some query, once its constants are normalized (N1Patterns.normalize_sql, the
|
|
114
|
+
# one the n1-patterns resource uses), ran more than once within this one request.
|
|
115
|
+
def n_plus_one?(queries)
|
|
116
|
+
queries.group_by { |q| MCP::Resources::N1Patterns.normalize_sql(q[:sql].to_s) }.any? { |_, group| group.size > 1 }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def controller_action(data)
|
|
120
|
+
value = TraceExporter.fetch(data, "request")[:controller_action] || TraceExporter.fetch(data, "request")["controller_action"]
|
|
121
|
+
value.to_s.empty? ? UNKNOWN : value.to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def method_name(method)
|
|
125
|
+
name = method.to_s.upcase
|
|
126
|
+
HTTP_METHODS.include?(name) ? name : "_OTHER"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def sql_verb(sql)
|
|
130
|
+
verb = sql.to_s[/\A\s*(\w+)/, 1].to_s.upcase
|
|
131
|
+
SQL_VERBS.include?(verb) ? verb : "OTHER"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/lib/profiler/otel/sdk.rb
CHANGED
|
@@ -9,38 +9,56 @@ module Profiler
|
|
|
9
9
|
# requires "opentelemetry/..." and the only one that reaches the OpenTelemetry constants, and
|
|
10
10
|
# it does so only once config.otel_enabled is true: the gem declares no dependency on
|
|
11
11
|
# OpenTelemetry (like stackprof, it is optional), so an application that never heard of it
|
|
12
|
-
# loads and profiles exactly as before.
|
|
13
|
-
#
|
|
12
|
+
# loads and profiles exactly as before. Traces and metrics share it: the detection of the
|
|
13
|
+
# application's own provider, the resource and the OTLP settings are the same, only the gems
|
|
14
|
+
# and the provider class differ (see SIGNALS). Each signal is decided on its own, so the
|
|
15
|
+
# trace gems without the metrics ones (or the reverse) gives one signal and a warning for the
|
|
16
|
+
# other.
|
|
14
17
|
module Sdk
|
|
15
18
|
LOCK = Monitor.new
|
|
16
|
-
REQUIRES = ["opentelemetry/sdk", "opentelemetry/exporter/otlp"].freeze
|
|
17
|
-
ENDPOINT_ENV = %w[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTEL_EXPORTER_OTLP_ENDPOINT].freeze
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
# What differs between the signals: the gems to load, the API's global provider accessor,
|
|
21
|
+
# the SDK class of a provider, and the standard endpoint variables.
|
|
22
|
+
SIGNALS = {
|
|
23
|
+
traces: {
|
|
24
|
+
requires: ["opentelemetry/sdk", "opentelemetry/exporter/otlp"],
|
|
25
|
+
gems: "opentelemetry-sdk and opentelemetry-exporter-otlp",
|
|
26
|
+
endpoint_env: %w[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTEL_EXPORTER_OTLP_ENDPOINT].freeze
|
|
27
|
+
},
|
|
28
|
+
metrics: {
|
|
29
|
+
requires: ["opentelemetry/sdk", "opentelemetry-metrics-sdk", "opentelemetry/exporter/otlp_metrics"],
|
|
30
|
+
gems: "opentelemetry-metrics-api, opentelemetry-metrics-sdk and opentelemetry-exporter-otlp-metrics " \
|
|
31
|
+
"(alpha, pin an exact version of each)",
|
|
32
|
+
endpoint_env: %w[OTEL_EXPORTER_OTLP_METRICS_ENDPOINT OTEL_EXPORTER_OTLP_ENDPOINT].freeze
|
|
33
|
+
}
|
|
34
|
+
}.freeze
|
|
35
|
+
ENDPOINT_ENV = SIGNALS.dig(:traces, :endpoint_env)
|
|
36
|
+
|
|
37
|
+
@state = {}
|
|
38
|
+
@available = {}
|
|
20
39
|
|
|
21
40
|
class << self
|
|
22
41
|
# The tracer provider spans are started from, nil when export is off or cannot work. Warns
|
|
23
42
|
# once (Profiler.log_error) when it cannot; never raises.
|
|
24
43
|
def tracer_provider
|
|
25
|
-
|
|
44
|
+
provider_for(:traces) { setup(:traces, :host_tracer_provider, :build_own_tracer_provider) { |p| scoped(p) } }
|
|
45
|
+
end
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Profiler.log_error("OpenTelemetry export disabled: setup failed", e)
|
|
32
|
-
nil
|
|
47
|
+
# The meter provider metrics are recorded with, nil when export is off or cannot work. The
|
|
48
|
+
# application's SDK MeterProvider when it has one, else our own, never installed globally.
|
|
49
|
+
def meter_provider
|
|
50
|
+
provider_for(:metrics) { setup(:metrics, :host_meter_provider, :build_own_meter_provider) }
|
|
33
51
|
end
|
|
34
52
|
|
|
35
|
-
# Whether the SDK and the OTLP exporter can be loaded. Loads them (the sole
|
|
36
|
-
# does), and says so in the log once if they cannot.
|
|
37
|
-
def available?
|
|
38
|
-
return @available unless @available.nil?
|
|
53
|
+
# Whether the SDK and the OTLP exporter of a signal can be loaded. Loads them (the sole
|
|
54
|
+
# place that does), and says so in the log once if they cannot.
|
|
55
|
+
def available?(signal = :traces)
|
|
56
|
+
return @available[signal] unless @available[signal].nil?
|
|
39
57
|
|
|
40
58
|
LOCK.synchronize do
|
|
41
|
-
return @available unless @available.nil?
|
|
59
|
+
return @available[signal] unless @available[signal].nil?
|
|
42
60
|
|
|
43
|
-
@available = load_gems
|
|
61
|
+
@available[signal] = load_gems(signal)
|
|
44
62
|
end
|
|
45
63
|
end
|
|
46
64
|
|
|
@@ -51,11 +69,14 @@ module Profiler
|
|
|
51
69
|
|
|
52
70
|
# The endpoint the profiler's own exporter posts to: otel_otlp_endpoint, else the standard
|
|
53
71
|
# OTEL_EXPORTER_OTLP_* variables the exporter itself reads. nil when neither is set.
|
|
54
|
-
def otlp_endpoint
|
|
72
|
+
def otlp_endpoint(signal = :traces)
|
|
55
73
|
configured = Profiler.configuration.otel_otlp_endpoint.to_s.strip
|
|
56
|
-
|
|
74
|
+
unless configured.empty?
|
|
75
|
+
# One setting serves both signals: the traces path means the metrics one.
|
|
76
|
+
return signal == :metrics ? configured.sub(%r{/v1/traces/?\z}, "/v1/metrics") : configured
|
|
77
|
+
end
|
|
57
78
|
|
|
58
|
-
|
|
79
|
+
SIGNALS.fetch(signal)[:endpoint_env].each do |name|
|
|
59
80
|
value = ENV[name].to_s.strip
|
|
60
81
|
return value unless value.empty?
|
|
61
82
|
end
|
|
@@ -65,33 +86,45 @@ module Profiler
|
|
|
65
86
|
# Forgets everything decided so far (the specs, and a configuration change).
|
|
66
87
|
def reset!
|
|
67
88
|
LOCK.synchronize do
|
|
68
|
-
@state =
|
|
69
|
-
@available =
|
|
89
|
+
@state = {}
|
|
90
|
+
@available = {}
|
|
70
91
|
end
|
|
71
92
|
end
|
|
72
93
|
|
|
73
94
|
private
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
# The provider of a signal: nil when export is off, decided once (in the block), nil with
|
|
97
|
+
# one logged error when the setup raises.
|
|
98
|
+
def provider_for(signal)
|
|
99
|
+
return nil unless Profiler.configuration.otel_enabled
|
|
100
|
+
|
|
101
|
+
state = @state[signal] || LOCK.synchronize { @state[signal] ||= yield }
|
|
102
|
+
state[:provider]
|
|
103
|
+
rescue StandardError, LoadError => e
|
|
104
|
+
LOCK.synchronize { @state[signal] = { provider: nil } }
|
|
105
|
+
Profiler.log_error("OpenTelemetry #{signal} export disabled: setup failed", e)
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_gems(signal)
|
|
110
|
+
SIGNALS.fetch(signal)[:requires].each { |name| require name }
|
|
77
111
|
true
|
|
78
112
|
rescue LoadError => e
|
|
79
113
|
Profiler.log_error(
|
|
80
|
-
"config.otel_enabled is true but the OpenTelemetry gems are not available
|
|
81
|
-
"add
|
|
82
|
-
"OpenTelemetry export is off."
|
|
114
|
+
"config.otel_enabled is true but the OpenTelemetry #{signal} gems are not available " \
|
|
115
|
+
"(#{e.message}); add #{SIGNALS.fetch(signal)[:gems]} to the application's Gemfile. " \
|
|
116
|
+
"OpenTelemetry #{signal} export is off."
|
|
83
117
|
)
|
|
84
118
|
false
|
|
85
119
|
end
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
existing = host_tracer_provider
|
|
91
|
-
return { provider: scoped(existing) } if existing
|
|
121
|
+
# The host's provider when it has one (then prepared by the block, if any), else our own.
|
|
122
|
+
def setup(signal, host_finder, builder)
|
|
123
|
+
return { provider: nil } unless available?(signal)
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
provider = send(host_finder) || send(builder)
|
|
126
|
+
provider = yield(provider) if provider && block_given?
|
|
127
|
+
{ provider: provider }
|
|
95
128
|
end
|
|
96
129
|
|
|
97
130
|
# The SDK provider the application initialised for itself, if any. The API's global
|
|
@@ -99,35 +132,56 @@ module Profiler
|
|
|
99
132
|
def host_tracer_provider
|
|
100
133
|
provider = OpenTelemetry.tracer_provider
|
|
101
134
|
provider = provider.delegate if provider.respond_to?(:delegate)
|
|
102
|
-
|
|
135
|
+
provider if defined?(OpenTelemetry::SDK::Trace::TracerProvider) &&
|
|
136
|
+
provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
|
|
103
137
|
end
|
|
104
138
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
139
|
+
def host_meter_provider
|
|
140
|
+
provider = OpenTelemetry.meter_provider
|
|
141
|
+
provider = provider.delegate if provider.respond_to?(:delegate)
|
|
142
|
+
provider if defined?(OpenTelemetry::SDK::Metrics::MeterProvider) &&
|
|
143
|
+
provider.is_a?(OpenTelemetry::SDK::Metrics::MeterProvider)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The endpoint of our own exporter, nil (after one logged error) when there is none.
|
|
147
|
+
def endpoint_or_warn(signal)
|
|
148
|
+
endpoint = otlp_endpoint(signal)
|
|
149
|
+
return endpoint if endpoint
|
|
150
|
+
|
|
151
|
+
Profiler.log_error(
|
|
152
|
+
"config.otel_enabled is true but there is no OpenTelemetry SDK in the application and " \
|
|
153
|
+
"config.otel_otlp_endpoint is not set: OpenTelemetry #{signal} export is off."
|
|
154
|
+
)
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def resource
|
|
159
|
+
OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
|
|
108
160
|
end
|
|
109
161
|
|
|
110
162
|
# Our own provider, never installed as the global one: the application's OpenTelemetry
|
|
111
163
|
# stays as it is. OTLP over http/protobuf, the protocol of opentelemetry-exporter-otlp.
|
|
112
164
|
def build_own_tracer_provider
|
|
113
|
-
endpoint =
|
|
114
|
-
unless endpoint
|
|
115
|
-
Profiler.log_error(
|
|
116
|
-
"config.otel_enabled is true but there is no OpenTelemetry SDK in the application and " \
|
|
117
|
-
"config.otel_otlp_endpoint is not set: OpenTelemetry export is off."
|
|
118
|
-
)
|
|
119
|
-
return nil
|
|
120
|
-
end
|
|
165
|
+
endpoint = endpoint_or_warn(:traces) or return nil
|
|
121
166
|
|
|
122
167
|
exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: endpoint)
|
|
123
|
-
provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
124
|
-
resource: OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
|
|
125
|
-
)
|
|
168
|
+
provider = OpenTelemetry::SDK::Trace::TracerProvider.new(resource: resource)
|
|
126
169
|
provider.add_span_processor(OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter))
|
|
127
170
|
at_exit { provider.shutdown }
|
|
128
171
|
provider
|
|
129
172
|
end
|
|
130
173
|
|
|
174
|
+
# Same, for metrics: a periodic reader pushes what was recorded to the OTLP metrics exporter.
|
|
175
|
+
def build_own_meter_provider
|
|
176
|
+
endpoint = endpoint_or_warn(:metrics) or return nil
|
|
177
|
+
|
|
178
|
+
exporter = OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(endpoint: endpoint)
|
|
179
|
+
provider = OpenTelemetry::SDK::Metrics::MeterProvider.new(resource: resource)
|
|
180
|
+
provider.add_metric_reader(OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(exporter: exporter))
|
|
181
|
+
at_exit { provider.shutdown }
|
|
182
|
+
provider
|
|
183
|
+
end
|
|
184
|
+
|
|
131
185
|
# The provider with its id generator wrapped, once.
|
|
132
186
|
def scoped(provider)
|
|
133
187
|
unless provider.respond_to?(:id_generator=)
|
data/lib/profiler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -235,6 +235,7 @@ files:
|
|
|
235
235
|
- lib/profiler/models/profile.rb
|
|
236
236
|
- lib/profiler/models/sql_query.rb
|
|
237
237
|
- lib/profiler/models/timeline_event.rb
|
|
238
|
+
- lib/profiler/otel/metrics_exporter.rb
|
|
238
239
|
- lib/profiler/otel/scoped_id_generator.rb
|
|
239
240
|
- lib/profiler/otel/sdk.rb
|
|
240
241
|
- lib/profiler/otel/trace_exporter.rb
|