rails-otel-context 0.9.4 → 0.9.7
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 +31 -0
- data/lib/rails_otel_context/adapters/mysql2.rb +3 -4
- data/lib/rails_otel_context/adapters/pg.rb +6 -8
- data/lib/rails_otel_context/adapters/trilogy.rb +3 -4
- data/lib/rails_otel_context/call_context_processor.rb +15 -12
- data/lib/rails_otel_context/frame_context.rb +15 -4
- data/lib/rails_otel_context/railtie.rb +5 -54
- data/lib/rails_otel_context/source_location.rb +16 -0
- data/lib/rails_otel_context/version.rb +1 -1
- data/lib/rails_otel_context.rb +61 -5
- 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: f8ea9eb65d7e405bbd14a39889bbad983edbba5c4bdcce252157d426a2372bc6
|
|
4
|
+
data.tar.gz: c70ddac87d64c8359fe82fe142bda62e4968b3429ea6944796b475270310d1d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfee7357103d1336be233b9b6724c1b6678117f7fcd3e57dda75ca473a3a608c8f511e7151db29cd9bce34627fcd8d0b482fcdb640812941c960385bf57acadd
|
|
7
|
+
data.tar.gz: 15888432fb759223a87cdf35f0dd9ae00c07ac59ef038b06bd49644968996b23fc910b0cca0f0912857bf8a4101ad9d005aa66596cef5b4a1e29de68a12486c8
|
data/README.md
CHANGED
|
@@ -118,6 +118,37 @@ RailsOtelContext.configure do |c|
|
|
|
118
118
|
end
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
## Conditional loading (`require: false`)
|
|
122
|
+
|
|
123
|
+
If your Gemfile has `require: false` and you load the gem from an initializer, call `RailsOtelContext.install!` explicitly. Loading the gem inside `config/initializers/` is too late for Rails to run the railtie's initializer hooks, so without an explicit `install!` call the AR subscriber and `around_action` hooks are never registered — `code.activerecord.*` and `rails.controller` will be absent from all spans.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Gemfile
|
|
127
|
+
gem 'rails-otel-context', '~> 0.9', require: false
|
|
128
|
+
|
|
129
|
+
# config/initializers/opentelemetry.rb
|
|
130
|
+
return unless ENV['ENABLE_OTLP']
|
|
131
|
+
|
|
132
|
+
require 'rails_otel_context'
|
|
133
|
+
|
|
134
|
+
RailsOtelContext.configure do |c|
|
|
135
|
+
c.span_name_formatter = lambda { |original, ar| ... }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
RailsOtelContext.install! # registers AR hooks, around_action, and the span processor
|
|
139
|
+
|
|
140
|
+
require 'opentelemetry/sdk'
|
|
141
|
+
require 'opentelemetry/exporter/otlp'
|
|
142
|
+
require 'opentelemetry/instrumentation/all'
|
|
143
|
+
|
|
144
|
+
OpenTelemetry::SDK.configure do |c|
|
|
145
|
+
c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'my_app')
|
|
146
|
+
c.use_all
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`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.
|
|
151
|
+
|
|
121
152
|
## How `code.namespace` / `code.function` works
|
|
122
153
|
|
|
123
154
|
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.
|
|
@@ -32,12 +32,11 @@ module RailsOtelContext
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# Push call-site into FrameContext BEFORE super so the OTel child span
|
|
36
|
+
# created inside super picks it up via CallContextProcessor#on_start.
|
|
36
37
|
%i[query prepare].each do |method_name|
|
|
37
38
|
define_method(method_name) do |*args|
|
|
38
|
-
|
|
39
|
-
mod.apply_call_site_to_span(OpenTelemetry::Trace.current_span, mod.call_site_for_app)
|
|
40
|
-
result
|
|
39
|
+
mod.with_call_site_frame { super(*args) }
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
end
|
|
@@ -42,16 +42,14 @@ module RailsOtelContext
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# Push call-site into FrameContext BEFORE super so the OTel child span
|
|
46
|
+
# created inside super picks it up via CallContextProcessor#on_start.
|
|
46
47
|
methods.each do |method_name|
|
|
47
48
|
define_method(method_name) do |*args, &user_block|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
super(*args) do |result|
|
|
53
|
-
mod.apply_call_site_to_span(OpenTelemetry::Trace.current_span, site)
|
|
54
|
-
user_block ? user_block.call(result) : result
|
|
49
|
+
mod.with_call_site_frame do
|
|
50
|
+
super(*args) do |result|
|
|
51
|
+
user_block ? user_block.call(result) : result
|
|
52
|
+
end
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|
|
@@ -32,11 +32,10 @@ module RailsOtelContext
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# Push call-site into FrameContext BEFORE super so the OTel child span
|
|
36
|
+
# created inside super picks it up via CallContextProcessor#on_start.
|
|
36
37
|
define_method(:query) do |sql|
|
|
37
|
-
|
|
38
|
-
mod.apply_call_site_to_span(OpenTelemetry::Trace.current_span, mod.call_site_for_app)
|
|
39
|
-
result
|
|
38
|
+
mod.with_call_site_frame { super(sql) }
|
|
40
39
|
end
|
|
41
40
|
end
|
|
42
41
|
|
|
@@ -71,26 +71,29 @@ module RailsOtelContext
|
|
|
71
71
|
nil
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
# Return Export::SUCCESS (0) so the SDK's tracer_provider.force_flush/shutdown
|
|
75
|
+
# can safely call results.max across all registered processors without raising
|
|
76
|
+
# ArgumentError when mixing our return value with integer status codes.
|
|
77
|
+
def force_flush(**) = 0
|
|
78
|
+
def shutdown(**) = 0
|
|
77
79
|
|
|
78
80
|
private
|
|
79
81
|
|
|
80
82
|
def apply_call_context(span)
|
|
81
83
|
# Explicit override: app code called FrameContext.with_frame (or Frameable).
|
|
82
84
|
# O(1) — no stack walk. Takes priority over automatic detection.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
end
|
|
85
|
+
# DB adapters push filepath/lineno too so the child span gets full call-site info.
|
|
86
|
+
site = FrameContext.current
|
|
87
|
+
unless site
|
|
88
|
+
# Default: walk the call stack to find the nearest app-code frame.
|
|
89
|
+
return unless Thread.respond_to?(:each_caller_location)
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
site = call_site_for_app
|
|
92
|
+
end
|
|
93
|
+
set_call_site_attributes(span, site)
|
|
94
|
+
end
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
def set_call_site_attributes(span, site)
|
|
94
97
|
return unless site
|
|
95
98
|
|
|
96
99
|
span.set_attribute('code.namespace', site[:class_name])
|
|
@@ -27,17 +27,19 @@ module RailsOtelContext
|
|
|
27
27
|
class << self
|
|
28
28
|
# Pushes +class_name+/+method_name+ for the duration of the block,
|
|
29
29
|
# restoring whatever was pushed before (supports nesting).
|
|
30
|
-
|
|
30
|
+
# Optional +filepath:+ and +lineno:+ are carried through to the span
|
|
31
|
+
# processor so DB adapter call-site info survives the span lifecycle.
|
|
32
|
+
def with_frame(class_name:, method_name:, filepath: nil, lineno: nil)
|
|
31
33
|
prev = Thread.current[FRAME_KEY]
|
|
32
|
-
Thread.current[FRAME_KEY] =
|
|
34
|
+
Thread.current[FRAME_KEY] = build_frame(class_name, method_name, filepath, lineno)
|
|
33
35
|
yield
|
|
34
36
|
ensure
|
|
35
37
|
Thread.current[FRAME_KEY] = prev
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
# Manual push without a block. Caller must call +pop+ in an ensure.
|
|
39
|
-
def push(class_name:, method_name:)
|
|
40
|
-
Thread.current[FRAME_KEY] =
|
|
41
|
+
def push(class_name:, method_name:, filepath: nil, lineno: nil)
|
|
42
|
+
Thread.current[FRAME_KEY] = build_frame(class_name, method_name, filepath, lineno)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Clears the pushed frame. Pair with +push+ in an ensure block.
|
|
@@ -51,6 +53,15 @@ module RailsOtelContext
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
alias clear! pop
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def build_frame(class_name, method_name, filepath, lineno)
|
|
60
|
+
frame = { class_name: class_name, method_name: method_name }
|
|
61
|
+
frame[:filepath] = filepath if filepath
|
|
62
|
+
frame[:lineno] = lineno if lineno
|
|
63
|
+
frame.freeze
|
|
64
|
+
end
|
|
54
65
|
end
|
|
55
66
|
end
|
|
56
67
|
|
|
@@ -5,23 +5,12 @@ require 'rails_otel_context/call_context_processor'
|
|
|
5
5
|
|
|
6
6
|
module RailsOtelContext
|
|
7
7
|
class Railtie < Rails::Railtie
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Runs after config/initializers/ so the OTel SDK tracer_provider is already configured.
|
|
8
|
+
# Runs after config/initializers/ so the OTel SDK tracer_provider is already
|
|
9
|
+
# configured. install! is idempotent — if the app already called
|
|
10
|
+
# RailsOtelContext.install! from an initializer this is a no-op for hooks,
|
|
11
|
+
# but install_processor! still runs (it self-guards with @processor_installed).
|
|
16
12
|
config.after_initialize do
|
|
17
|
-
RailsOtelContext.
|
|
18
|
-
|
|
19
|
-
# Warm the table→model map once at boot (after eager_load! in production so
|
|
20
|
-
# all descendants are available). Without this, the first SQL-named span on a
|
|
21
|
-
# cold boot hits an empty map and falls through without model context.
|
|
22
|
-
ActiveSupport.on_load(:active_record) do
|
|
23
|
-
RailsOtelContext::ActiveRecordContext.ar_table_model_map
|
|
24
|
-
end
|
|
13
|
+
RailsOtelContext.install!
|
|
25
14
|
end
|
|
26
15
|
|
|
27
16
|
# Reset the table→model map after every code reload in development.
|
|
@@ -31,43 +20,5 @@ module RailsOtelContext
|
|
|
31
20
|
config.to_prepare do
|
|
32
21
|
RailsOtelContext::ActiveRecordContext.reset_ar_table_model_map!
|
|
33
22
|
end
|
|
34
|
-
|
|
35
|
-
# Capture controller + action for every request and propagate them to all
|
|
36
|
-
# child spans via RequestContext. Also resets the N+1 query counter at both
|
|
37
|
-
# the start and end of every request to prevent bleed across Puma thread reuse.
|
|
38
|
-
# Always-on — no config gate.
|
|
39
|
-
#
|
|
40
|
-
# Both hooks are required: ActionController::Base fires :action_controller,
|
|
41
|
-
# ActionController::API (Rails API-only apps) fires :action_controller_api.
|
|
42
|
-
# In Rails 8 API-only apps :action_controller never fires, so without the
|
|
43
|
-
# second hook rails.controller / rails.action would be absent from every span.
|
|
44
|
-
initializer 'rails_otel_context.install_request_context' do
|
|
45
|
-
around_action_hook = proc do
|
|
46
|
-
around_action do |_controller, block|
|
|
47
|
-
RailsOtelContext::RequestContext.set(
|
|
48
|
-
controller: self.class.name,
|
|
49
|
-
action: action_name
|
|
50
|
-
)
|
|
51
|
-
block.call
|
|
52
|
-
ensure
|
|
53
|
-
RailsOtelContext::RequestContext.clear!
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
ActiveSupport.on_load(:action_controller, &around_action_hook)
|
|
57
|
-
ActiveSupport.on_load(:action_controller_api, &around_action_hook)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Capture job class name for every ActiveJob execution and propagate it to all
|
|
61
|
-
# child spans via RequestContext so rails.job appears on every span in the job.
|
|
62
|
-
initializer 'rails_otel_context.install_job_context' do
|
|
63
|
-
ActiveSupport.on_load(:active_job) do
|
|
64
|
-
around_perform do |_job, block|
|
|
65
|
-
RailsOtelContext::RequestContext.set_job(job_class: self.class.name)
|
|
66
|
-
block.call
|
|
67
|
-
ensure
|
|
68
|
-
RailsOtelContext::RequestContext.clear_job!
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
23
|
end
|
|
73
24
|
end
|
|
@@ -46,6 +46,22 @@ module RailsOtelContext
|
|
|
46
46
|
span.set_attribute('code.lineno', site[:lineno]) if site[:lineno]
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Wraps a block with the nearest app-code frame pushed into FrameContext.
|
|
50
|
+
# Used by DB adapters to make the call-site available to
|
|
51
|
+
# CallContextProcessor#on_start for the child span created inside the block.
|
|
52
|
+
# Uses with_frame (not push/pop) so nested frames are correctly restored.
|
|
53
|
+
def with_call_site_frame(&)
|
|
54
|
+
site = call_site_for_app
|
|
55
|
+
if site
|
|
56
|
+
FrameContext.with_frame(
|
|
57
|
+
class_name: site[:class_name], method_name: site[:method_name],
|
|
58
|
+
filepath: site[:filepath], lineno: site[:lineno], &
|
|
59
|
+
)
|
|
60
|
+
else
|
|
61
|
+
yield
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
# Legacy helper kept for Redis and ClickHouse adapters that only need filepath + lineno.
|
|
50
66
|
# Migrate those adapters to call_site_for_app + apply_call_site_to_span to remove this.
|
|
51
67
|
def source_location_for_app
|
data/lib/rails_otel_context.rb
CHANGED
|
@@ -30,13 +30,32 @@ module RailsOtelContext
|
|
|
30
30
|
@configuration = Configuration.new
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Full installation: registers all Rails hooks (AR adapters, around_action,
|
|
34
|
+
# around_perform) and the CallContextProcessor. Safe to call from a
|
|
35
|
+
# config/initializers file when the gem is loaded with require: false:
|
|
36
|
+
#
|
|
37
|
+
# # config/initializers/opentelemetry.rb
|
|
38
|
+
# return unless ENV['ENABLE_OTLP']
|
|
39
|
+
# require 'rails_otel_context'
|
|
40
|
+
# RailsOtelContext.configure { |c| ... }
|
|
41
|
+
# RailsOtelContext.install!
|
|
42
|
+
#
|
|
43
|
+
# The Railtie calls this automatically via after_initialize, so apps that
|
|
44
|
+
# let Bundler auto-require the gem do not need to call it explicitly.
|
|
45
|
+
# Safe to call multiple times — idempotent.
|
|
46
|
+
def install!(app_root: nil)
|
|
47
|
+
app_root ||= Rails.root if defined?(Rails)
|
|
48
|
+
register_hooks!(app_root) unless @hooks_installed
|
|
49
|
+
install_processor!
|
|
50
|
+
end
|
|
51
|
+
|
|
33
52
|
# Registers CallContextProcessor with the OTel tracer_provider.
|
|
34
|
-
# Called automatically by
|
|
35
|
-
#
|
|
36
|
-
# after_initialize block):
|
|
53
|
+
# Called automatically by install!. Call this manually only when the OTel
|
|
54
|
+
# SDK is configured after install! has already run (rare):
|
|
37
55
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
56
|
+
# RailsOtelContext.install! # hooks up AR/request context
|
|
57
|
+
# OpenTelemetry::SDK.configure { … } # SDK configured later
|
|
58
|
+
# RailsOtelContext.install_processor! # add processor to the now-real provider
|
|
40
59
|
#
|
|
41
60
|
# Safe to call multiple times — idempotent.
|
|
42
61
|
def install_processor!
|
|
@@ -49,6 +68,43 @@ module RailsOtelContext
|
|
|
49
68
|
OpenTelemetry.tracer_provider.add_span_processor(processor)
|
|
50
69
|
end
|
|
51
70
|
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def register_hooks!(app_root)
|
|
74
|
+
@hooks_installed = true
|
|
75
|
+
|
|
76
|
+
ActiveSupport.on_load(:active_record) do
|
|
77
|
+
RailsOtelContext::Adapters.install!(app_root: app_root, config: RailsOtelContext.configuration)
|
|
78
|
+
RailsOtelContext::ActiveRecordContext.install!(app_root: app_root)
|
|
79
|
+
RailsOtelContext::ActiveRecordContext.ar_table_model_map
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
around_action_hook = proc do
|
|
83
|
+
around_action do |_controller, block|
|
|
84
|
+
RailsOtelContext::RequestContext.set(
|
|
85
|
+
controller: self.class.name,
|
|
86
|
+
action: action_name
|
|
87
|
+
)
|
|
88
|
+
block.call
|
|
89
|
+
ensure
|
|
90
|
+
RailsOtelContext::RequestContext.clear!
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
ActiveSupport.on_load(:action_controller, &around_action_hook)
|
|
94
|
+
ActiveSupport.on_load(:action_controller_api, &around_action_hook)
|
|
95
|
+
|
|
96
|
+
ActiveSupport.on_load(:active_job) do
|
|
97
|
+
around_perform do |_job, block|
|
|
98
|
+
RailsOtelContext::RequestContext.set_job(job_class: self.class.name)
|
|
99
|
+
block.call
|
|
100
|
+
ensure
|
|
101
|
+
RailsOtelContext::RequestContext.clear_job!
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
public
|
|
107
|
+
|
|
52
108
|
# Convenience delegates to FrameContext — see FrameContext for full docs.
|
|
53
109
|
def with_frame(class_name:, method_name:, &block)
|
|
54
110
|
FrameContext.with_frame(class_name: class_name, method_name: method_name, &block)
|