rails-otel-context 0.8.1 → 0.8.5
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/lib/rails_otel_context/activerecord_context.rb +15 -34
- data/lib/rails_otel_context/adapters/clickhouse.rb +2 -2
- data/lib/rails_otel_context/adapters/redis.rb +14 -26
- data/lib/rails_otel_context/call_context_processor.rb +23 -4
- data/lib/rails_otel_context/railtie.rb +7 -0
- data/lib/rails_otel_context/version.rb +1 -1
- 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: c77108c1ddfab7776d31052f6633e7ec132aafb415f682099f571e5c4758db89
|
|
4
|
+
data.tar.gz: 262506de5a8bc5a4bbfa74341bdb79113fd54f4d0fe6d247c51cb35db395b006
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6dd47b5fcaea795a44e9e2db032fd77ccfddb97bef173be6ff1c19499e52e5ffbe26ff3b8a9ce8a5d4bf145f2f2aa58cd115aac04ac7ebf166d6dc98d0401ae6
|
|
7
|
+
data.tar.gz: 5ac0a1d07185865a92f79bab9895b04f5405b7f4a4978f6ded96878ad1b4bb0ed76c093bcd1a7ab912f5c1935f6989fb692fc4dd9d9000a2f211b294c777123d
|
|
@@ -13,12 +13,10 @@ module RailsOtelContext
|
|
|
13
13
|
# Transaction.recent_completed.to_a where the scope method returns before
|
|
14
14
|
# SQL fires.
|
|
15
15
|
module ActiveRecordContext
|
|
16
|
-
THREAD_KEY
|
|
17
|
-
SCOPE_THREAD_KEY
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
DB_SLOW_ATTR = 'db.slow'
|
|
21
|
-
private_constant :THREAD_KEY, :SCOPE_THREAD_KEY, :TIMING_ID_KEY, :TIMING_START_KEY
|
|
16
|
+
THREAD_KEY = :_rails_otel_ctx_ar
|
|
17
|
+
SCOPE_THREAD_KEY = :_rails_otel_ctx_scope
|
|
18
|
+
DB_SLOW_ATTR = 'db.slow'
|
|
19
|
+
private_constant :THREAD_KEY, :SCOPE_THREAD_KEY
|
|
22
20
|
|
|
23
21
|
# Frozen regex — only the verb regex remains; table extraction uses index+slice.
|
|
24
22
|
SQL_VERB_RE = /\A(\w+)/i
|
|
@@ -81,16 +79,11 @@ module RailsOtelContext
|
|
|
81
79
|
|
|
82
80
|
# Subscriber for sql.active_record notifications.
|
|
83
81
|
class Subscriber
|
|
84
|
-
def
|
|
85
|
-
@threshold = RailsOtelContext.configuration.slow_query_threshold_ms
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def start(_name, id, payload)
|
|
82
|
+
def start(_name, _id, payload)
|
|
89
83
|
ar_name = payload[:name]
|
|
90
|
-
return
|
|
91
|
-
return if ar_name == 'SCHEMA' || ar_name.start_with?('CACHE')
|
|
84
|
+
return if ar_name == 'SCHEMA' || ar_name&.start_with?('CACHE')
|
|
92
85
|
|
|
93
|
-
ctx = if ar_name == 'SQL'
|
|
86
|
+
ctx = if ar_name.nil? || ar_name == 'SQL'
|
|
94
87
|
ActiveRecordContext.parse_sql_context(payload[:sql])
|
|
95
88
|
else
|
|
96
89
|
ActiveRecordContext.parse_ar_name(ar_name)
|
|
@@ -109,11 +102,6 @@ module RailsOtelContext
|
|
|
109
102
|
ctx[:async] = true if payload[:async]
|
|
110
103
|
Thread.current[THREAD_KEY] = ctx
|
|
111
104
|
|
|
112
|
-
if @threshold
|
|
113
|
-
Thread.current[TIMING_ID_KEY] = id
|
|
114
|
-
Thread.current[TIMING_START_KEY] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
105
|
# Enrich the current span directly. When OTel instruments via driver-level
|
|
118
106
|
# prepend (Trilogy, PG, Mysql2), the span is created BEFORE this notification
|
|
119
107
|
# fires, so CallContextProcessor#on_start sees nil AR context. Applying here
|
|
@@ -123,15 +111,7 @@ module RailsOtelContext
|
|
|
123
111
|
ActiveRecordContext.apply_to_span(OpenTelemetry::Trace.current_span, ctx)
|
|
124
112
|
end
|
|
125
113
|
|
|
126
|
-
def finish(_name,
|
|
127
|
-
if @threshold && Thread.current[TIMING_ID_KEY].equal?(id)
|
|
128
|
-
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - Thread.current[TIMING_START_KEY]) * 1000
|
|
129
|
-
Thread.current[TIMING_ID_KEY] = nil
|
|
130
|
-
if elapsed_ms >= @threshold
|
|
131
|
-
span = OpenTelemetry::Trace.current_span
|
|
132
|
-
span.set_attribute(DB_SLOW_ATTR, true) if span.context.valid?
|
|
133
|
-
end
|
|
134
|
-
end
|
|
114
|
+
def finish(_name, _id, _payload)
|
|
135
115
|
ensure
|
|
136
116
|
Thread.current[THREAD_KEY] = nil
|
|
137
117
|
end
|
|
@@ -195,8 +175,6 @@ module RailsOtelContext
|
|
|
195
175
|
def clear!
|
|
196
176
|
Thread.current[THREAD_KEY] = nil
|
|
197
177
|
Thread.current[SCOPE_THREAD_KEY] = nil
|
|
198
|
-
Thread.current[TIMING_ID_KEY] = nil
|
|
199
|
-
Thread.current[TIMING_START_KEY] = nil
|
|
200
178
|
end
|
|
201
179
|
|
|
202
180
|
# Test helpers: set AR context directly for unit tests.
|
|
@@ -280,11 +258,14 @@ module RailsOtelContext
|
|
|
280
258
|
end
|
|
281
259
|
return nil unless keyword
|
|
282
260
|
|
|
283
|
-
table
|
|
284
|
-
|
|
261
|
+
table = extract_table_after(sql, keyword)
|
|
262
|
+
model_name = table ? ar_table_model_map[table] : nil
|
|
285
263
|
|
|
286
|
-
|
|
287
|
-
|
|
264
|
+
# Fall back to the virtual "SQL" model when the table cannot be resolved
|
|
265
|
+
# (e.g. SELECT SLEEP(0.2), SELECT 1, raw DDL). This lets the span-name
|
|
266
|
+
# formatter produce "SQL.Select" / "SQL.Update" for tab-group purposes
|
|
267
|
+
# instead of leaving the span unnamed.
|
|
268
|
+
model_name ||= 'SQL'
|
|
288
269
|
|
|
289
270
|
{ model_name: model_name, method_name: verb,
|
|
290
271
|
query_key: "#{model_name}.#{verb}".freeze }
|
|
@@ -64,7 +64,7 @@ module RailsOtelContext
|
|
|
64
64
|
define_method(method_name) do |*args, &block|
|
|
65
65
|
return super(*args, &block) if Thread.current[reentrancy_key]
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
site = mod.call_site_for_app
|
|
68
68
|
statement = args.first.is_a?(String) ? args.first : nil
|
|
69
69
|
|
|
70
70
|
tracer = OpenTelemetry.tracer_provider.tracer('rails-otel-context-clickhouse')
|
|
@@ -76,7 +76,7 @@ module RailsOtelContext
|
|
|
76
76
|
span.set_attribute('db.statement', statement) if statement
|
|
77
77
|
|
|
78
78
|
result = super(*args, &block)
|
|
79
|
-
mod.
|
|
79
|
+
mod.apply_call_site_to_span(span, site)
|
|
80
80
|
result
|
|
81
81
|
end
|
|
82
82
|
ensure
|
|
@@ -24,47 +24,35 @@ module RailsOtelContext
|
|
|
24
24
|
def build_patch_module
|
|
25
25
|
mod = Module.new do
|
|
26
26
|
class << self
|
|
27
|
+
include RailsOtelContext::SourceLocation
|
|
28
|
+
|
|
27
29
|
attr_accessor :app_root
|
|
28
30
|
|
|
29
31
|
def configure(app_root:)
|
|
30
32
|
@app_root = app_root.to_s
|
|
31
33
|
end
|
|
32
|
-
|
|
33
|
-
def source_location_for_app
|
|
34
|
-
return unless Thread.respond_to?(:each_caller_location)
|
|
35
|
-
|
|
36
|
-
Thread.each_caller_location do |location|
|
|
37
|
-
path = location.absolute_path || location.path
|
|
38
|
-
next unless path&.start_with?(app_root)
|
|
39
|
-
next if path.include?('/gems/')
|
|
40
|
-
|
|
41
|
-
return [path.delete_prefix("#{app_root}/"), location.lineno]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
nil
|
|
45
|
-
end
|
|
46
34
|
end
|
|
47
35
|
|
|
48
36
|
define_method(:call) do |command, redis_config, &block|
|
|
49
|
-
|
|
50
|
-
return super(command, redis_config, &block) unless
|
|
37
|
+
site = mod.call_site_for_app
|
|
38
|
+
return super(command, redis_config, &block) unless site
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
) do
|
|
40
|
+
attrs = { 'code.namespace' => site[:class_name], 'code.filepath' => site[:filepath] }
|
|
41
|
+
attrs['code.function'] = site[:method_name] if site[:method_name]
|
|
42
|
+
attrs['code.lineno'] = site[:lineno] if site[:lineno]
|
|
43
|
+
OpenTelemetry::Instrumentation::Redis.with_attributes(attrs) do
|
|
56
44
|
super(command, redis_config, &block)
|
|
57
45
|
end
|
|
58
46
|
end
|
|
59
47
|
|
|
60
48
|
define_method(:call_pipelined) do |commands, redis_config, &block|
|
|
61
|
-
|
|
62
|
-
return super(commands, redis_config, &block) unless
|
|
49
|
+
site = mod.call_site_for_app
|
|
50
|
+
return super(commands, redis_config, &block) unless site
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
) do
|
|
52
|
+
attrs = { 'code.namespace' => site[:class_name], 'code.filepath' => site[:filepath] }
|
|
53
|
+
attrs['code.function'] = site[:method_name] if site[:method_name]
|
|
54
|
+
attrs['code.lineno'] = site[:lineno] if site[:lineno]
|
|
55
|
+
OpenTelemetry::Instrumentation::Redis.with_attributes(attrs) do
|
|
68
56
|
super(commands, redis_config, &block)
|
|
69
57
|
end
|
|
70
58
|
end
|
|
@@ -39,9 +39,10 @@ module RailsOtelContext
|
|
|
39
39
|
|
|
40
40
|
def initialize(app_root:, config: RailsOtelContext.configuration)
|
|
41
41
|
@app_root = app_root.to_s
|
|
42
|
-
@request_context_enabled
|
|
43
|
-
@custom_span_attributes
|
|
44
|
-
@span_name_formatter
|
|
42
|
+
@request_context_enabled = config.request_context_enabled
|
|
43
|
+
@custom_span_attributes = config.custom_span_attributes
|
|
44
|
+
@span_name_formatter = config.span_name_formatter
|
|
45
|
+
@slow_query_threshold_ms = config.slow_query_threshold_ms
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def on_start(span, _parent_context)
|
|
@@ -51,7 +52,25 @@ module RailsOtelContext
|
|
|
51
52
|
apply_custom_attributes(span) if @custom_span_attributes
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
def on_finish(
|
|
55
|
+
def on_finish(span)
|
|
56
|
+
return unless @slow_query_threshold_ms
|
|
57
|
+
return unless span.respond_to?(:attributes) && span.attributes&.key?('db.system')
|
|
58
|
+
|
|
59
|
+
start_ns = span.start_timestamp
|
|
60
|
+
end_ns = span.end_timestamp
|
|
61
|
+
return unless start_ns && end_ns
|
|
62
|
+
|
|
63
|
+
duration_ms = (end_ns - start_ns) / 1_000_000.0
|
|
64
|
+
return unless duration_ms >= @slow_query_threshold_ms
|
|
65
|
+
|
|
66
|
+
# span.recording? is false here — the span has finished and current_span
|
|
67
|
+
# has reverted to the HTTP parent. Write directly to the backing attributes
|
|
68
|
+
# hash so db.slow lands on the actual DB span, not the HTTP parent.
|
|
69
|
+
attrs = span.instance_variable_get(:@attributes)
|
|
70
|
+
attrs.store(ActiveRecordContext::DB_SLOW_ATTR, true) if attrs.respond_to?(:store)
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
55
74
|
|
|
56
75
|
def force_flush(timeout: nil); end
|
|
57
76
|
|
|
@@ -41,10 +41,17 @@ module RailsOtelContext
|
|
|
41
41
|
initializer 'rails_otel_context.install_frame_context' do
|
|
42
42
|
ActiveSupport.on_load(:action_controller) do
|
|
43
43
|
around_action do |_controller, block|
|
|
44
|
+
# Reset N+1 query counter at the start of every request so counts
|
|
45
|
+
# never bleed across requests on Puma's reused threads. This runs
|
|
46
|
+
# regardless of request_context_enabled, which only gates whether
|
|
47
|
+
# RequestContext.set is called (and would reset it there too).
|
|
48
|
+
Thread.current[RailsOtelContext::RequestContext::QUERY_COUNT_KEY] = nil
|
|
44
49
|
RailsOtelContext::FrameContext.with_frame(
|
|
45
50
|
class_name: self.class.name,
|
|
46
51
|
method_name: action_name
|
|
47
52
|
) { block.call }
|
|
53
|
+
ensure
|
|
54
|
+
Thread.current[RailsOtelContext::RequestContext::QUERY_COUNT_KEY] = nil
|
|
48
55
|
end
|
|
49
56
|
end
|
|
50
57
|
end
|