Skip to content

Commit 6d32bf7

Browse files
committed
feat: Instrument PG connect
1 parent 24a4a7c commit 6d32bf7

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

instrumentation/pg/lib/opentelemetry/instrumentation/pg/instrumentation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def require_dependencies
4040

4141
def patch_client
4242
::PG::Connection.prepend(Patches::Connection)
43+
::PG::Connection.singleton_class.prepend(Patches::ClassMethods)
4344
end
4445
end
4546
end

instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,43 @@ module OpenTelemetry
1212
module Instrumentation
1313
module PG
1414
module Patches
15+
# Module to prepend to PG::Connection singleton class for connection initialization
16+
module ClassMethods
17+
def new(*args, &block)
18+
tracer = PG::Instrumentation.instance.tracer
19+
config = PG::Instrumentation.instance.config
20+
21+
tracer.in_span('connect', kind: :client) do |span|
22+
conn = super
23+
24+
# Build attributes from the established connection
25+
attributes = {
26+
'db.system' => 'postgresql',
27+
'db.name' => conn.db,
28+
'db.user' => conn.user
29+
}
30+
attributes['peer.service'] = config[:peer_service] if config[:peer_service]
31+
32+
# Add transport attributes
33+
h = conn.host
34+
if h&.start_with?('/')
35+
attributes['net.sock.family'] = 'unix'
36+
attributes['net.peer.name'] = h
37+
else
38+
attributes['net.transport'] = 'ip_tcp'
39+
attributes['net.peer.name'] = h
40+
attributes['net.peer.port'] = conn.port if defined?(::PG::DEF_PGPORT)
41+
end
42+
43+
attributes.merge!(OpenTelemetry::Instrumentation::PG.attributes)
44+
attributes.compact!
45+
46+
span.add_attributes(attributes)
47+
conn
48+
end
49+
end
50+
end
51+
1552
# Module to prepend to PG::Connection for instrumentation
1653
module Connection # rubocop:disable Metrics/ModuleLength
1754
# Capture the first word (including letters, digits, underscores, & '.', ) that follows common table commands

instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,113 @@
6464
_(exporter.finished_spans.size).must_equal 0
6565
end
6666

67+
describe 'connection initialization' do
68+
it 'creates a connect span when establishing a connection' do
69+
conn = PG::Connection.open(
70+
host: host,
71+
port: port,
72+
user: user,
73+
dbname: dbname,
74+
password: password
75+
)
76+
conn.close
77+
78+
connect_span = exporter.finished_spans.first
79+
_(connect_span.name).must_equal 'connect'
80+
_(connect_span.kind).must_equal :client
81+
_(connect_span.attributes['db.system']).must_equal 'postgresql'
82+
_(connect_span.attributes['db.name']).must_equal dbname
83+
_(connect_span.attributes['db.user']).must_equal user
84+
_(connect_span.attributes['net.peer.name']).must_equal host
85+
_(connect_span.attributes['net.transport']).must_equal 'ip_tcp'
86+
end
87+
88+
it 'creates a connect span using PG::Connection.new' do
89+
conn = PG::Connection.new(
90+
host: host,
91+
port: port,
92+
user: user,
93+
dbname: dbname,
94+
password: password
95+
)
96+
conn.close
97+
98+
connect_span = exporter.finished_spans.first
99+
_(connect_span.name).must_equal 'connect'
100+
_(connect_span.kind).must_equal :client
101+
_(connect_span.attributes['db.system']).must_equal 'postgresql'
102+
_(connect_span.attributes['db.name']).must_equal dbname
103+
end
104+
105+
it 'creates a connect span using PG.connect' do
106+
conn = PG.connect(
107+
host: host,
108+
port: port,
109+
user: user,
110+
dbname: dbname,
111+
password: password
112+
)
113+
conn.close
114+
115+
connect_span = exporter.finished_spans.first
116+
_(connect_span.name).must_equal 'connect'
117+
_(connect_span.kind).must_equal :client
118+
_(connect_span.attributes['db.system']).must_equal 'postgresql'
119+
end
120+
121+
it 'accepts peer service name from config for connection' do
122+
instrumentation.instance_variable_set(:@installed, false)
123+
instrumentation.install(peer_service: 'readonly:postgres')
124+
125+
conn = PG::Connection.open(
126+
host: host,
127+
port: port,
128+
user: user,
129+
dbname: dbname,
130+
password: password
131+
)
132+
conn.close
133+
134+
connect_span = exporter.finished_spans.first
135+
_(connect_span.attributes['peer.service']).must_equal 'readonly:postgres'
136+
end
137+
138+
it 'sets unix socket attributes when connecting via socket' do
139+
skip 'socket test requires no host/port' if host || port
140+
141+
conn = PG::Connection.open(
142+
user: user,
143+
dbname: dbname,
144+
password: password
145+
)
146+
conn.close
147+
148+
connect_span = exporter.finished_spans.first
149+
_(connect_span.name).must_equal 'connect'
150+
_(connect_span.attributes['db.system']).must_equal 'postgresql'
151+
_(connect_span.attributes['net.sock.family']).must_equal 'unix'
152+
_(connect_span.attributes['net.peer.name']).must_match %r{^/}
153+
end
154+
155+
it 'records connection errors' do
156+
expect do
157+
PG::Connection.open(
158+
host: host,
159+
port: port,
160+
user: 'invalid_user',
161+
dbname: dbname,
162+
password: 'wrong_password'
163+
)
164+
end.must_raise PG::ConnectionBad
165+
166+
connect_span = exporter.finished_spans.first
167+
_(connect_span.name).must_equal 'connect'
168+
_(connect_span.status.code).must_equal OpenTelemetry::Trace::Status::ERROR
169+
_(connect_span.events.first.name).must_equal 'exception'
170+
_(connect_span.events.first.attributes['exception.type']).must_equal 'PG::ConnectionBad'
171+
end
172+
end
173+
67174
it 'accepts peer service name from config' do
68175
instrumentation.instance_variable_set(:@installed, false)
69176
instrumentation.install(peer_service: 'readonly:postgres')

0 commit comments

Comments
 (0)