rails-otel-context 0.9.9 → 0.9.11
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/README.md +17 -2
- data/lib/rails_otel_context/activerecord_context.rb +63 -12
- data/lib/rails_otel_context/version.rb +1 -1
- data/lib/rails_otel_context.rb +22 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2aec3d090a6f44452bd03610c73ccc2f06793896736c2d71c46433ac06f508a9
|
|
4
|
+
data.tar.gz: 3eb8e4627ec08271bfba16bca39b205e09bd1ee60f40bc218fbb745a138ad18b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a080a6664fac57852d9f774390cd6b938aaef8b4d2149cd204815d31248855d396dd4e8023ba1f111bd63b222351f42668d2908256477dca46170256edf8792a
|
|
7
|
+
data.tar.gz: 5b22380700691214524ba6539084748c47de7e76cd2238c905f3aee788823126ebcdd624169252cef54fe2693267c7535d141e2849ee4c7c22f4e5780a4a747b
|
data/README.md
CHANGED
|
@@ -140,8 +140,8 @@ RailsOtelContext.configure do |c|
|
|
|
140
140
|
c.span_name_formatter = lambda { |original, ar| ... }
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
# Configure the OTel SDK before calling install! so the processor is
|
|
144
|
+
# registered on the real TracerProvider, not the pre-configure proxy.
|
|
145
145
|
require 'opentelemetry/sdk'
|
|
146
146
|
require 'opentelemetry/exporter/otlp'
|
|
147
147
|
require 'opentelemetry/instrumentation/all'
|
|
@@ -150,10 +150,25 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
150
150
|
c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'my_app')
|
|
151
151
|
c.use_all
|
|
152
152
|
end
|
|
153
|
+
|
|
154
|
+
RailsOtelContext.install! # registers AR hooks, around_action, and the span processor
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
`install!` is idempotent — the railtie calls it automatically via `after_initialize`, so apps that let Bundler auto-require the gem do not need to call it.
|
|
156
158
|
|
|
159
|
+
### Two-initializer pattern
|
|
160
|
+
|
|
161
|
+
If your OTel SDK setup lives in a separate initializer that loads after the gem (e.g., Rails loads `rails_otel_context.rb` before `tracing.rb` alphabetically), call `install_processor!` at the end of the SDK initializer. Since 0.9.10, `install_processor!` tracks provider identity and re-registers automatically when `SDK.configure` has replaced the provider:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# config/initializers/tracing.rb (loads after rails_otel_context.rb)
|
|
165
|
+
OpenTelemetry::SDK.configure do |c|
|
|
166
|
+
c.use_all
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
RailsOtelContext.install_processor! if defined?(RailsOtelContext)
|
|
170
|
+
```
|
|
171
|
+
|
|
157
172
|
## How `code.namespace` / `code.function` works
|
|
158
173
|
|
|
159
174
|
On every span start, the gem walks the Ruby call stack (`Thread.each_caller_location`) and finds the first frame inside `Rails.root`. That frame becomes the four `code.*` attributes.
|
|
@@ -46,6 +46,16 @@ module RailsOtelContext
|
|
|
46
46
|
def singleton_method_added(name)
|
|
47
47
|
super
|
|
48
48
|
|
|
49
|
+
# Internal aliases created below (and by ScopeNameTracking) re-trigger
|
|
50
|
+
# this hook; wrapping them would route calls back through the wrapper
|
|
51
|
+
# and break receiver dispatch.
|
|
52
|
+
return if name.to_s.start_with?('__otel')
|
|
53
|
+
|
|
54
|
+
# ScopeNameTracking's redefinition of a scope method also re-triggers
|
|
55
|
+
# this hook. That method is already wrapped — wrapping it again would
|
|
56
|
+
# add a useless closure hop and leak a __otel_cm_orig_* alias.
|
|
57
|
+
return if @_otel_wrapped_scopes&.key?(name)
|
|
58
|
+
|
|
49
59
|
@_otel_wrapped_class_methods ||= {}
|
|
50
60
|
return if @_otel_wrapped_class_methods[name]
|
|
51
61
|
|
|
@@ -63,11 +73,14 @@ module RailsOtelContext
|
|
|
63
73
|
|
|
64
74
|
# Mark before define_singleton_method to prevent re-entrancy for this name
|
|
65
75
|
@_otel_wrapped_class_methods[name] = true
|
|
66
|
-
name_str
|
|
67
|
-
|
|
76
|
+
name_str = name.to_s.freeze
|
|
77
|
+
alias_name = :"__otel_cm_orig_#{name}"
|
|
78
|
+
# Alias instead of capturing a bound Method so inherited class methods
|
|
79
|
+
# keep self = the actual receiver (see ScopeNameTracking).
|
|
80
|
+
singleton_class.alias_method(alias_name, name)
|
|
68
81
|
|
|
69
82
|
define_singleton_method(name) do |*args, **kwargs, &blk|
|
|
70
|
-
result =
|
|
83
|
+
result = send(alias_name, *args, **kwargs, &blk)
|
|
71
84
|
if defined?(::ActiveRecord::Relation) && result.is_a?(::ActiveRecord::Relation)
|
|
72
85
|
result.instance_variable_set(:@_otel_scope_name, name_str)
|
|
73
86
|
end
|
|
@@ -132,26 +145,40 @@ module RailsOtelContext
|
|
|
132
145
|
# Wraps scope-generated class methods to store the scope name on the Relation.
|
|
133
146
|
module ScopeNameTracking
|
|
134
147
|
def scope(name, body, &)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
148
|
+
# Guard against double-wrapping on class reload in development.
|
|
149
|
+
# Marked BEFORE super so ClassMethodScopeTracking's
|
|
150
|
+
# singleton_method_added hook (which fires for both the scope macro's
|
|
151
|
+
# definition and our redefinition below) sees the name as owned by
|
|
152
|
+
# this module and skips it.
|
|
138
153
|
@_otel_wrapped_scopes ||= {}
|
|
139
|
-
return if @_otel_wrapped_scopes[name]
|
|
154
|
+
return super if @_otel_wrapped_scopes[name]
|
|
140
155
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
@_otel_wrapped_scopes[name] = true
|
|
157
|
+
super
|
|
158
|
+
|
|
159
|
+
name_str = name.to_s.freeze
|
|
160
|
+
alias_name = :"__otel_scope_orig_#{name}"
|
|
161
|
+
# Alias instead of capturing a bound Method: a bound Method locks self
|
|
162
|
+
# to the defining class, so inherited scopes would run with self =
|
|
163
|
+
# parent and re-evaluate default_scope in the wrong class context.
|
|
164
|
+
# send(alias_name) dispatches with self = the actual receiver.
|
|
165
|
+
singleton_class.alias_method(alias_name, name)
|
|
166
|
+
define_singleton_method(name) do |*args, **kwargs, &blk|
|
|
167
|
+
relation = send(alias_name, *args, **kwargs, &blk)
|
|
145
168
|
if relation.is_a?(::ActiveRecord::Relation)
|
|
146
169
|
relation.instance_variable_set(:@_otel_scope_name, name_str)
|
|
147
170
|
end
|
|
148
171
|
relation
|
|
149
172
|
end
|
|
150
|
-
@_otel_wrapped_scopes[name] = true
|
|
151
173
|
end
|
|
152
174
|
end
|
|
153
175
|
|
|
154
176
|
# Captures scope name from Relation at SQL materialization time.
|
|
177
|
+
#
|
|
178
|
+
# exec_queries covers record-loading (.to_a, .load). Aggregate and existence
|
|
179
|
+
# queries take separate paths that never call exec_queries -- count/sum/etc.
|
|
180
|
+
# go through #calculate, and #pluck / #exists? are their own methods -- so each
|
|
181
|
+
# needs the same scope-key capture to tag its sql.active_record span.
|
|
155
182
|
module RelationScopeCapture
|
|
156
183
|
def exec_queries(&)
|
|
157
184
|
scope_name = instance_variable_get(:@_otel_scope_name)
|
|
@@ -160,6 +187,30 @@ module RailsOtelContext
|
|
|
160
187
|
ensure
|
|
161
188
|
Thread.current[SCOPE_THREAD_KEY] = nil
|
|
162
189
|
end
|
|
190
|
+
|
|
191
|
+
def calculate(*args, **kwargs, &)
|
|
192
|
+
scope_name = instance_variable_get(:@_otel_scope_name)
|
|
193
|
+
Thread.current[SCOPE_THREAD_KEY] = scope_name if scope_name
|
|
194
|
+
super
|
|
195
|
+
ensure
|
|
196
|
+
Thread.current[SCOPE_THREAD_KEY] = nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def pluck(*args, &)
|
|
200
|
+
scope_name = instance_variable_get(:@_otel_scope_name)
|
|
201
|
+
Thread.current[SCOPE_THREAD_KEY] = scope_name if scope_name
|
|
202
|
+
super
|
|
203
|
+
ensure
|
|
204
|
+
Thread.current[SCOPE_THREAD_KEY] = nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def exists?(*args)
|
|
208
|
+
scope_name = instance_variable_get(:@_otel_scope_name)
|
|
209
|
+
Thread.current[SCOPE_THREAD_KEY] = scope_name if scope_name
|
|
210
|
+
super
|
|
211
|
+
ensure
|
|
212
|
+
Thread.current[SCOPE_THREAD_KEY] = nil
|
|
213
|
+
end
|
|
163
214
|
end
|
|
164
215
|
|
|
165
216
|
module_function
|
data/lib/rails_otel_context.rb
CHANGED
|
@@ -51,22 +51,35 @@ module RailsOtelContext
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Registers CallContextProcessor with the OTel tracer_provider.
|
|
54
|
-
# Called automatically by install!.
|
|
55
|
-
# SDK
|
|
54
|
+
# Called automatically by install!. Safe to call manually after a second
|
|
55
|
+
# OpenTelemetry::SDK.configure call replaces the tracer_provider — the
|
|
56
|
+
# method detects the provider change and re-registers automatically:
|
|
56
57
|
#
|
|
57
58
|
# RailsOtelContext.install! # hooks up AR/request context
|
|
58
|
-
# OpenTelemetry::SDK.configure { … } # SDK
|
|
59
|
-
# RailsOtelContext.install_processor! #
|
|
59
|
+
# OpenTelemetry::SDK.configure { … } # second SDK configure (replaces provider)
|
|
60
|
+
# RailsOtelContext.install_processor! # re-registers on the new provider
|
|
60
61
|
#
|
|
61
|
-
#
|
|
62
|
+
# Idempotent per provider instance: re-registers when OpenTelemetry::SDK.configure
|
|
63
|
+
# is called again (which replaces the global tracer_provider with a new object),
|
|
64
|
+
# but skips registration when called multiple times against the same provider.
|
|
62
65
|
def install_processor!
|
|
63
|
-
return if @processor_installed
|
|
64
66
|
return unless defined?(Rails) && Rails.root
|
|
65
|
-
return unless
|
|
67
|
+
return unless defined?(OpenTelemetry)
|
|
68
|
+
|
|
69
|
+
# Capture once: tracer_provider is a global that SDK.configure can replace
|
|
70
|
+
# on another thread. A single local read makes the rest of the method consistent.
|
|
71
|
+
provider = OpenTelemetry.tracer_provider
|
|
72
|
+
return unless provider.respond_to?(:add_span_processor)
|
|
73
|
+
|
|
74
|
+
# Use object identity, not a boolean flag. SDK.configure creates a new
|
|
75
|
+
# TracerProvider instance each time, so equal? detects provider replacement
|
|
76
|
+
# and triggers re-registration, while guarding against double-registration
|
|
77
|
+
# on the same provider.
|
|
78
|
+
return if @processor_registered_on.equal?(provider)
|
|
66
79
|
|
|
67
|
-
@
|
|
80
|
+
@processor_registered_on = provider
|
|
68
81
|
processor = RailsOtelContext::CallContextProcessor.new(app_root: Rails.root)
|
|
69
|
-
|
|
82
|
+
provider.add_span_processor(processor)
|
|
70
83
|
end
|
|
71
84
|
|
|
72
85
|
private
|