pinot-client 1.27.0 → 1.28.0
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/pinot/active_support_notifications.rb +3 -2
- data/lib/pinot/instrumentation.rb +68 -12
- data/lib/pinot/open_telemetry.rb +160 -0
- data/lib/pinot/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da3d726029acb7167db3fef1a14951e4f4c46beac51f32e88e1e7782eab64fa1
|
|
4
|
+
data.tar.gz: 4271e56a42a4d269bdf8de68a4c631e8320ec2eec4a36deeb3b635e10e8dc933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef69651b14484fceeec26edca916ede91f04ea3224f24d449e2cf022a3bb847e9ca0d429908daad14e10daa72c9bd4719f4e2400dff06431dca1c6126c90bd7e
|
|
7
|
+
data.tar.gz: 8ab20c872830e2387245fadebae91e2739b5e30eb959a9769346df4652e0a3e18d673da2e6a3f88ae5753e55e41e7ed21ae709827d920e338e569444ba4a878a
|
|
@@ -48,7 +48,7 @@ module Pinot
|
|
|
48
48
|
def self.install!
|
|
49
49
|
return if installed?
|
|
50
50
|
|
|
51
|
-
Pinot::Instrumentation.
|
|
51
|
+
@listener = Pinot::Instrumentation.subscribe(method(:notify))
|
|
52
52
|
@installed = true
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -57,7 +57,8 @@ module Pinot
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def self.uninstall!
|
|
60
|
-
Pinot::Instrumentation.
|
|
60
|
+
Pinot::Instrumentation.unsubscribe(@listener) if @listener
|
|
61
|
+
@listener = nil
|
|
61
62
|
@installed = false
|
|
62
63
|
end
|
|
63
64
|
|
|
@@ -1,32 +1,92 @@
|
|
|
1
1
|
module Pinot
|
|
2
2
|
# Low-level instrumentation hook that fires after every query executed via
|
|
3
3
|
# Connection#execute_sql. This is the extension point used by
|
|
4
|
-
# Pinot::ActiveSupportNotifications and any custom
|
|
4
|
+
# Pinot::ActiveSupportNotifications, Pinot::OpenTelemetry, and any custom
|
|
5
|
+
# observability layer.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
+
# == Subscribing (multiple listeners supported)
|
|
7
8
|
#
|
|
8
|
-
# Pinot::Instrumentation.
|
|
9
|
+
# listener = Pinot::Instrumentation.subscribe(->(event) do
|
|
9
10
|
# MyMetrics.record(event[:table], event[:duration_ms], event[:success])
|
|
11
|
+
# end)
|
|
12
|
+
#
|
|
13
|
+
# # Remove later:
|
|
14
|
+
# Pinot::Instrumentation.unsubscribe(listener)
|
|
15
|
+
#
|
|
16
|
+
# == Legacy single-callback API (still supported)
|
|
17
|
+
#
|
|
18
|
+
# Pinot::Instrumentation.on_query = ->(event) { ... }
|
|
19
|
+
# Pinot::Instrumentation.on_query = nil # remove
|
|
20
|
+
#
|
|
21
|
+
# == Around-execution hook (for OTel and similar span-based tools)
|
|
22
|
+
#
|
|
23
|
+
# The `around` hook wraps the entire query execution — the block yields the
|
|
24
|
+
# query, and the hook is responsible for calling Instrumentation.notify when
|
|
25
|
+
# done. Only one around hook can be registered at a time.
|
|
26
|
+
#
|
|
27
|
+
# Pinot::Instrumentation.around = ->(table:, query:) do
|
|
28
|
+
# MyTracer.in_span("pinot") { yield }
|
|
10
29
|
# end
|
|
11
30
|
#
|
|
12
|
-
#
|
|
31
|
+
# == Event Hash keys
|
|
32
|
+
#
|
|
13
33
|
# :table => String — table name passed to execute_sql
|
|
14
34
|
# :query => String — SQL string
|
|
15
35
|
# :duration_ms => Float — wall-clock time in milliseconds
|
|
16
36
|
# :success => Boolean — false when an exception was raised
|
|
17
37
|
# :error => Exception or nil — the exception on failure, nil on success
|
|
18
|
-
#
|
|
19
|
-
# Only one callback can be registered at a time. Set on_query= to nil to remove it.
|
|
20
38
|
module Instrumentation
|
|
39
|
+
@listeners = []
|
|
40
|
+
@around = nil
|
|
41
|
+
|
|
42
|
+
# Add a post-execution listener. Returns the listener so it can be passed
|
|
43
|
+
# to unsubscribe later.
|
|
44
|
+
def self.subscribe(listener)
|
|
45
|
+
@listeners << listener
|
|
46
|
+
listener
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Remove a previously subscribed listener.
|
|
50
|
+
def self.unsubscribe(listener)
|
|
51
|
+
@listeners.delete(listener)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Register an around-execution wrapper. Only one wrapper is supported;
|
|
55
|
+
# the new value replaces any previous one. Set to nil to remove.
|
|
56
|
+
def self.around=(wrapper)
|
|
57
|
+
@around = wrapper
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.around
|
|
61
|
+
@around
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Legacy single-callback setter. Replaces all listeners with the given
|
|
65
|
+
# callback (or clears them when nil).
|
|
21
66
|
def self.on_query=(callback)
|
|
22
|
-
@
|
|
67
|
+
@listeners = callback ? [callback] : []
|
|
23
68
|
end
|
|
24
69
|
|
|
70
|
+
# Returns the first registered listener (legacy compat).
|
|
25
71
|
def self.on_query
|
|
26
|
-
@
|
|
72
|
+
@listeners.first
|
|
27
73
|
end
|
|
28
74
|
|
|
29
75
|
def self.instrument(table:, query:)
|
|
76
|
+
if @around
|
|
77
|
+
@around.call(table: table, query: query) { yield }
|
|
78
|
+
else
|
|
79
|
+
_timed_instrument(table: table, query: query) { yield }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fire all registered listeners with an event hash. Called by the default
|
|
84
|
+
# instrument path and by the OTel around wrapper.
|
|
85
|
+
def self.notify(event)
|
|
86
|
+
@listeners.each { |l| l.call(event) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private_class_method def self._timed_instrument(table:, query:)
|
|
30
90
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
91
|
result = yield
|
|
32
92
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
@@ -37,9 +97,5 @@ module Pinot
|
|
|
37
97
|
notify(table: table, query: query, duration_ms: duration_ms, success: false, error: e)
|
|
38
98
|
raise
|
|
39
99
|
end
|
|
40
|
-
|
|
41
|
-
def self.notify(event)
|
|
42
|
-
@on_query&.call(event)
|
|
43
|
-
end
|
|
44
100
|
end
|
|
45
101
|
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
require "pinot"
|
|
2
|
+
|
|
3
|
+
module Pinot
|
|
4
|
+
# Opt-in OpenTelemetry bridge.
|
|
5
|
+
#
|
|
6
|
+
# == Setup
|
|
7
|
+
#
|
|
8
|
+
# Add to an initializer after the opentelemetry SDK is configured:
|
|
9
|
+
#
|
|
10
|
+
# require "pinot/open_telemetry"
|
|
11
|
+
# Pinot::OpenTelemetry.install!
|
|
12
|
+
#
|
|
13
|
+
# == What it does
|
|
14
|
+
#
|
|
15
|
+
# Each call to Connection#execute_sql (and anything built on top of it —
|
|
16
|
+
# execute_sql_with_params, execute_many, PreparedStatement) creates an OTel
|
|
17
|
+
# span named "pinot.query" with attributes following the OpenTelemetry
|
|
18
|
+
# semantic conventions for database spans:
|
|
19
|
+
#
|
|
20
|
+
# db.system = "pinot"
|
|
21
|
+
# db.statement = "<sql>" (the full SQL string)
|
|
22
|
+
# db.name = "<table>" (the Pinot table name)
|
|
23
|
+
# db.operation = "SELECT" (first token of the SQL)
|
|
24
|
+
#
|
|
25
|
+
# On failure the span is marked with error status and the exception is
|
|
26
|
+
# recorded on the span.
|
|
27
|
+
#
|
|
28
|
+
# == Trace-context propagation
|
|
29
|
+
#
|
|
30
|
+
# When installed, every outbound HTTP request to a broker is injected with
|
|
31
|
+
# W3C Trace Context headers (traceparent / tracestate) so distributed traces
|
|
32
|
+
# flow through Pinot. This relies on OpenTelemetry.propagation being
|
|
33
|
+
# configured (the default SDK sets this up automatically).
|
|
34
|
+
#
|
|
35
|
+
# == Feature flag
|
|
36
|
+
#
|
|
37
|
+
# The bridge can be toggled at runtime without reinstalling:
|
|
38
|
+
#
|
|
39
|
+
# Pinot::OpenTelemetry.enabled = false # pause tracing (e.g. in tests)
|
|
40
|
+
# Pinot::OpenTelemetry.enabled = true # resume
|
|
41
|
+
# Pinot::OpenTelemetry.enabled? # => true / false
|
|
42
|
+
#
|
|
43
|
+
# == Lifecycle
|
|
44
|
+
#
|
|
45
|
+
# Pinot::OpenTelemetry.install! # idempotent
|
|
46
|
+
# Pinot::OpenTelemetry.installed? # => true
|
|
47
|
+
# Pinot::OpenTelemetry.uninstall! # removes hooks; leaves transport unpatched
|
|
48
|
+
#
|
|
49
|
+
# Note: this gem does NOT depend on opentelemetry-api or opentelemetry-sdk.
|
|
50
|
+
# Both must be present and initialized before install! is called.
|
|
51
|
+
module OpenTelemetry
|
|
52
|
+
SPAN_NAME = "pinot.query"
|
|
53
|
+
DB_SYSTEM = "pinot"
|
|
54
|
+
TRACER_NAME = "pinot-client"
|
|
55
|
+
|
|
56
|
+
@installed = false
|
|
57
|
+
@enabled = true
|
|
58
|
+
|
|
59
|
+
# Enable or disable tracing at runtime without uninstalling.
|
|
60
|
+
def self.enabled=(val)
|
|
61
|
+
@enabled = val ? true : false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.enabled?
|
|
65
|
+
@enabled
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.install!
|
|
69
|
+
return if installed?
|
|
70
|
+
|
|
71
|
+
_install_around_hook
|
|
72
|
+
_patch_transport
|
|
73
|
+
@installed = true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.installed?
|
|
77
|
+
@installed
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.uninstall!
|
|
81
|
+
::Pinot::Instrumentation.around = nil
|
|
82
|
+
@installed = false
|
|
83
|
+
# Note: JsonHttpTransport prepend is permanent once applied (Ruby limitation).
|
|
84
|
+
# Disable the propagator by unsetting the flag — it no-ops when disabled.
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# -------------------------------------------------------------------------
|
|
88
|
+
# Internal helpers
|
|
89
|
+
# -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def self._install_around_hook
|
|
92
|
+
::Pinot::Instrumentation.around = method(:_around)
|
|
93
|
+
end
|
|
94
|
+
private_class_method :_install_around_hook
|
|
95
|
+
|
|
96
|
+
def self._around(table:, query:)
|
|
97
|
+
unless @enabled
|
|
98
|
+
# Bypass tracing; still time and notify listeners.
|
|
99
|
+
return ::Pinot::Instrumentation.send(:_timed_instrument, table: table, query: query) { yield }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
tracer = ::OpenTelemetry.tracer_provider.tracer(TRACER_NAME, ::Pinot::VERSION)
|
|
103
|
+
attrs = _span_attributes(table, query)
|
|
104
|
+
|
|
105
|
+
tracer.in_span(SPAN_NAME, attributes: attrs, kind: :client) do |span|
|
|
106
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
107
|
+
begin
|
|
108
|
+
result = yield
|
|
109
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
110
|
+
::Pinot::Instrumentation.notify(
|
|
111
|
+
table: table, query: query, duration_ms: duration_ms, success: true, error: nil
|
|
112
|
+
)
|
|
113
|
+
result
|
|
114
|
+
rescue => e
|
|
115
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
116
|
+
span.record_exception(e)
|
|
117
|
+
span.status = ::OpenTelemetry::Trace::Status.error(e.message)
|
|
118
|
+
::Pinot::Instrumentation.notify(
|
|
119
|
+
table: table, query: query, duration_ms: duration_ms, success: false, error: e
|
|
120
|
+
)
|
|
121
|
+
raise
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
private_class_method :_around
|
|
126
|
+
|
|
127
|
+
def self._span_attributes(table, query)
|
|
128
|
+
attrs = {
|
|
129
|
+
"db.system" => DB_SYSTEM,
|
|
130
|
+
"db.statement" => query,
|
|
131
|
+
"db.name" => table.to_s
|
|
132
|
+
}
|
|
133
|
+
op = query.to_s.lstrip.split(/\s+/, 2).first&.upcase
|
|
134
|
+
attrs["db.operation"] = op if op && !op.empty?
|
|
135
|
+
attrs
|
|
136
|
+
end
|
|
137
|
+
private_class_method :_span_attributes
|
|
138
|
+
|
|
139
|
+
# Patch JsonHttpTransport to inject W3C trace-context headers into every
|
|
140
|
+
# outbound HTTP request. Applied once via Module#prepend.
|
|
141
|
+
def self._patch_transport
|
|
142
|
+
return if ::Pinot::JsonHttpTransport.ancestors.include?(TraceContextInjector)
|
|
143
|
+
::Pinot::JsonHttpTransport.prepend(TraceContextInjector)
|
|
144
|
+
end
|
|
145
|
+
private_class_method :_patch_transport
|
|
146
|
+
|
|
147
|
+
# Prepended into JsonHttpTransport. Injects traceparent/tracestate into
|
|
148
|
+
# request headers when the bridge is enabled and a current span exists.
|
|
149
|
+
module TraceContextInjector
|
|
150
|
+
def execute(broker_address, request, extra_request_headers: {})
|
|
151
|
+
otel = ::Pinot::OpenTelemetry
|
|
152
|
+
return super unless otel.installed? && otel.enabled?
|
|
153
|
+
|
|
154
|
+
carrier = {}
|
|
155
|
+
::OpenTelemetry.propagation.inject(carrier)
|
|
156
|
+
super(broker_address, request, extra_request_headers: extra_request_headers.merge(carrier))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/pinot/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pinot-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Xiang Fu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- lib/pinot/grpc_transport.rb
|
|
117
117
|
- lib/pinot/instrumentation.rb
|
|
118
118
|
- lib/pinot/logger.rb
|
|
119
|
+
- lib/pinot/open_telemetry.rb
|
|
119
120
|
- lib/pinot/paginator.rb
|
|
120
121
|
- lib/pinot/prepared_statement.rb
|
|
121
122
|
- lib/pinot/proto/broker_service.proto
|