Skip to content

Commit caf6622

Browse files
authored
Merge pull request #1153 from code0-tech/1147-setup-proper-tracing-for-actioncable
Create proper spans for ActionCable
2 parents 88129d4 + a80f664 commit caf6622

6 files changed

Lines changed: 347 additions & 24 deletions

File tree

app/channels/application_cable/channel.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
module ApplicationCable
44
class Channel < ActionCable::Channel::Base
5+
def self.tracer
6+
@tracer ||= ::OpenTelemetry.tracer_provider.tracer('sagittarius-cable')
7+
end
8+
59
def subscribe_to_channel
6-
with_context { super }
10+
with_otel_span("#{self.class.name} subscribe", 'subscribe') do
11+
with_context { super }
12+
end
713
end
814

915
def perform_action(data)
10-
with_context { super }
16+
with_otel_span("#{self.class.name} #{data['action']}", 'process') do
17+
with_context { super }
18+
end
1119
end
1220

1321
protected
@@ -30,6 +38,30 @@ def create_authentication(token_type, token)
3038
end
3139
end
3240

41+
def otel_context
42+
return ::OpenTelemetry::Context.current unless connection.respond_to?(:otel_context)
43+
44+
connection.otel_context || ::OpenTelemetry::Context.current
45+
end
46+
47+
def with_otel_span(span_name, operation, parent_context: otel_context, links: [], &block)
48+
attributes = {
49+
'messaging.system' => 'action_cable',
50+
'messaging.operation' => operation,
51+
'code.namespace' => self.class.name,
52+
}
53+
54+
::OpenTelemetry::Context.with_current(parent_context) do
55+
self.class.tracer.in_span(span_name, kind: :server, attributes: attributes, links: links) do |span|
56+
block.call(span)
57+
rescue StandardError => e
58+
span.record_exception(e)
59+
span.status = ::OpenTelemetry::Trace::Status.error(e.message)
60+
raise
61+
end
62+
end
63+
end
64+
3365
def with_context(&block)
3466
Code0::ZeroTrack::Context.with_context(
3567
application: 'cable',

app/channels/application_cable/connection.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,18 @@
22

33
module ApplicationCable
44
class Connection < ActionCable::Connection::Base
5+
attr_reader :otel_context
6+
7+
def connect
8+
@otel_context = extract_otel_context
9+
end
10+
11+
private
12+
13+
def extract_otel_context
14+
::OpenTelemetry.propagation.extract(request.headers)
15+
rescue StandardError
16+
::OpenTelemetry::Context.current
17+
end
518
end
619
end

app/channels/graphql_channel.rb

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,53 @@ def subscribed
1111
end
1212

1313
def execute(data)
14-
result = SagittariusSchema.execute(
15-
query: data['query'],
16-
context: {
17-
current_authentication: find_authentication(@token),
18-
visibility_profile: :execution,
19-
channel: self,
20-
},
21-
variables: data['variables'],
22-
operation_name: data['operationName']
23-
)
24-
25-
@subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
26-
27-
transmit({ result: result.to_h, more: result.subscription? })
14+
connection_span_context = ::OpenTelemetry::Trace.current_span(otel_context).context
15+
links = if connection_span_context.valid?
16+
[::OpenTelemetry::Trace::Link.new(connection_span_context)]
17+
else
18+
[]
19+
end
20+
21+
with_otel_span("#{self.class.name} execute", 'process',
22+
parent_context: ::OpenTelemetry::Context::ROOT, links: links) do |span|
23+
span.set_attribute('graphql.operation.name', data['operationName']) if data['operationName']
24+
25+
result = SagittariusSchema.execute(
26+
query: data['query'],
27+
context: {
28+
current_authentication: find_authentication(@token),
29+
visibility_profile: :execution,
30+
channel: self,
31+
},
32+
variables: data['variables'],
33+
operation_name: data['operationName']
34+
)
35+
36+
@subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
37+
38+
transmit({ result: result.to_h, more: result.subscription? })
39+
end
2840
end
2941

3042
def unsubscribed
31-
@subscription_ids.each do |sid|
32-
SagittariusSchema.subscriptions.delete_subscription(sid)
43+
with_otel_span("#{self.class.name} unsubscribed", 'process') do
44+
@subscription_ids.each do |sid|
45+
SagittariusSchema.subscriptions.delete_subscription(sid)
46+
end
3347
end
3448
end
3549

3650
private
3751

3852
def verify_authentication
39-
with_context do
40-
authentication = find_authentication(@token)
41-
return unless authentication.invalid? || authentication.none?
42-
43-
@subscription_ids.each { |sid| SagittariusSchema.subscriptions.delete_subscription(sid) }
44-
reject
53+
with_otel_span("#{self.class.name} verify_authentication", 'process') do
54+
with_context do
55+
authentication = find_authentication(@token)
56+
return unless authentication.invalid? || authentication.none?
57+
58+
@subscription_ids.each { |sid| SagittariusSchema.subscriptions.delete_subscription(sid) }
59+
reject
60+
end
4561
end
4662
end
4763
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe ApplicationCable::Channel do
6+
include AuthenticationHelpers
7+
include ActionCable::Channel::TestCase::Behavior
8+
9+
include_context 'with graphql subscription support'
10+
11+
tests GraphqlChannel
12+
13+
let(:exporter) { OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new }
14+
let(:span_processor) { OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter) }
15+
let(:tracer_provider) do
16+
OpenTelemetry::SDK::Trace::TracerProvider.new.tap do |tp|
17+
tp.add_span_processor(span_processor)
18+
end
19+
end
20+
let(:tracer) { tracer_provider.tracer('sagittarius-cable') }
21+
22+
let(:user) { create(:user) }
23+
let(:token) { "Session #{authorization_token(user)}" }
24+
25+
before do
26+
propagator = OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator.new
27+
allow(OpenTelemetry).to receive(:propagation).and_return(propagator)
28+
allow(described_class).to receive(:tracer).and_return(tracer)
29+
allow(GraphqlChannel).to receive(:tracer).and_return(tracer)
30+
end
31+
32+
describe '#subscribe_to_channel' do
33+
it 'creates a span with correct name' do
34+
stub_connection(otel_context: OpenTelemetry::Context.current)
35+
subscribe(token: token)
36+
37+
span = exporter.finished_spans.find { |s| s.name.include?('subscribe') }
38+
expect(span.name).to eq('GraphqlChannel subscribe')
39+
end
40+
41+
it 'sets messaging attributes' do
42+
stub_connection(otel_context: OpenTelemetry::Context.current)
43+
subscribe(token: token)
44+
45+
span = exporter.finished_spans.find { |s| s.name.include?('subscribe') }
46+
expect(span.attributes['messaging.system']).to eq('action_cable')
47+
expect(span.attributes['messaging.operation']).to eq('subscribe')
48+
expect(span.attributes['code.namespace']).to eq('GraphqlChannel')
49+
end
50+
51+
context 'when connection has a trace context' do
52+
let(:parent_span_context) do
53+
OpenTelemetry::Trace::SpanContext.new(
54+
trace_id: OpenTelemetry::Trace.generate_trace_id,
55+
span_id: OpenTelemetry::Trace.generate_span_id,
56+
trace_flags: OpenTelemetry::Trace::TraceFlags::SAMPLED
57+
)
58+
end
59+
let(:parent_context) do
60+
OpenTelemetry::Trace.context_with_span(
61+
OpenTelemetry::Trace.non_recording_span(parent_span_context)
62+
)
63+
end
64+
65+
it 'creates spans as children of the connection context' do
66+
stub_connection(otel_context: parent_context)
67+
subscribe(token: token)
68+
69+
span = exporter.finished_spans.find { |s| s.name.include?('subscribe') }
70+
expect(span.hex_trace_id).to eq(parent_span_context.hex_trace_id)
71+
expect(span.hex_parent_span_id).to eq(parent_span_context.hex_span_id)
72+
end
73+
end
74+
end
75+
76+
describe '#perform_action' do
77+
before do
78+
stub_connection(otel_context: OpenTelemetry::Context.current)
79+
subscribe(token: token)
80+
end
81+
82+
it 'creates a span with the action name' do
83+
perform :execute,
84+
query: 'subscription($message: String) { echo(message: $message) { message } }',
85+
variables: { message: 'hello' }
86+
87+
span = exporter.finished_spans.find { |s| s.name == 'GraphqlChannel execute' }
88+
expect(span).not_to be_nil
89+
end
90+
91+
it 'sets messaging attributes' do
92+
perform :execute,
93+
query: 'subscription($message: String) { echo(message: $message) { message } }',
94+
variables: { message: 'hello' }
95+
96+
span = exporter.finished_spans.find { |s| s.name == 'GraphqlChannel execute' }
97+
expect(span).not_to be_nil
98+
expect(span.attributes['messaging.system']).to eq('action_cable')
99+
expect(span.attributes['code.namespace']).to eq('GraphqlChannel')
100+
end
101+
end
102+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe ApplicationCable::Connection do
6+
let(:propagator) { OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator.new }
7+
8+
before do
9+
allow(OpenTelemetry).to receive(:propagation).and_return(propagator)
10+
end
11+
12+
describe '#connect' do
13+
context 'when traceparent is present in upgrade request headers' do
14+
it 'extracts and stores the trace context' do
15+
connect '/cable', headers: { 'Traceparent' => '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' }
16+
17+
span_context = OpenTelemetry::Trace.current_span(connection.otel_context).context
18+
expect(span_context.hex_trace_id).to eq('0af7651916cd43dd8448eb211c80319c')
19+
expect(span_context.hex_span_id).to eq('b7ad6b7169203331')
20+
end
21+
end
22+
23+
context 'when no trace headers are present' do
24+
it 'stores a context without error' do
25+
connect '/cable'
26+
27+
expect(connection.otel_context).not_to be_nil
28+
end
29+
end
30+
31+
context 'when invalid traceparent is present' do
32+
it 'stores a context without error' do
33+
connect '/cable', headers: { 'Traceparent' => 'garbage-value' }
34+
35+
expect(connection.otel_context).not_to be_nil
36+
end
37+
end
38+
end
39+
end

0 commit comments

Comments
 (0)