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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfd22c5e76430105c17c3a1279b002084aebb894d4c561913908a4d00038b519
4
- data.tar.gz: e668583cdd1afde9b9b35dab28c46b2e707643911a0800cdb98950ac1de2731b
3
+ metadata.gz: da3d726029acb7167db3fef1a14951e4f4c46beac51f32e88e1e7782eab64fa1
4
+ data.tar.gz: 4271e56a42a4d269bdf8de68a4c631e8320ec2eec4a36deeb3b635e10e8dc933
5
5
  SHA512:
6
- metadata.gz: 1aba59c0e39a601dacccfa2f413b9405ad0835e94555272a8f7bf8177b421fd0eac861144fb44d24c69ec41f51834b67a786ed4bd29afb48ae6667671f5ce112
7
- data.tar.gz: c81b393e62ffba2390a7c21a52fb0b0a3f0fdfd0421b3a78d9bcbcc772a35f2613e451f58a28cdcb9ecd30c6120aa114f94ae3a80c825594a0cc9baa98a521ca
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.on_query = method(:notify)
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.on_query = nil
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 observability layer.
4
+ # Pinot::ActiveSupportNotifications, Pinot::OpenTelemetry, and any custom
5
+ # observability layer.
5
6
  #
6
- # Register a single callback:
7
+ # == Subscribing (multiple listeners supported)
7
8
  #
8
- # Pinot::Instrumentation.on_query = ->(event) do
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
- # The event Hash contains:
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
- @on_query = callback
67
+ @listeners = callback ? [callback] : []
23
68
  end
24
69
 
70
+ # Returns the first registered listener (legacy compat).
25
71
  def self.on_query
26
- @on_query
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
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.27.0"
2
+ VERSION = "1.28.0"
3
3
  end
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.27.0
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-19 00:00:00.000000000 Z
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