clickhouse-ruby 0.2.0 → 0.3.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/CHANGELOG.md +67 -0
- data/README.md +142 -1
- data/lib/clickhouse_ruby/active_record/generators/migration_generator.rb +308 -0
- data/lib/clickhouse_ruby/active_record/generators/templates/create_table.rb.tt +59 -0
- data/lib/clickhouse_ruby/active_record/generators/templates/migration.rb.tt +87 -0
- data/lib/clickhouse_ruby/active_record/railtie.rb +16 -2
- data/lib/clickhouse_ruby/active_record/schema_dumper.rb +458 -0
- data/lib/clickhouse_ruby/active_record.rb +1 -0
- data/lib/clickhouse_ruby/client.rb +212 -1
- data/lib/clickhouse_ruby/connection_pool.rb +73 -1
- data/lib/clickhouse_ruby/instrumentation.rb +176 -0
- data/lib/clickhouse_ruby/version.rb +1 -1
- data/lib/clickhouse_ruby.rb +1 -0
- metadata +20 -1
|
@@ -167,6 +167,91 @@ module ClickhouseRuby
|
|
|
167
167
|
result.first["version"]
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# Returns the query execution plan for a SQL query
|
|
171
|
+
#
|
|
172
|
+
# Uses EXPLAIN to show how ClickHouse will execute the query.
|
|
173
|
+
#
|
|
174
|
+
# @param sql [String] the SQL query to explain
|
|
175
|
+
# @param type [Symbol] type of explain (:plan, :pipeline, :estimate, :ast, :syntax)
|
|
176
|
+
# @param settings [Hash] ClickHouse settings
|
|
177
|
+
# @return [Array<Hash>] the query plan rows
|
|
178
|
+
#
|
|
179
|
+
# @example Basic explain
|
|
180
|
+
# client.explain('SELECT * FROM events WHERE date > today()')
|
|
181
|
+
# # => [{"explain" => "Expression ..."}]
|
|
182
|
+
#
|
|
183
|
+
# @example Pipeline explain
|
|
184
|
+
# client.explain('SELECT count() FROM events', type: :pipeline)
|
|
185
|
+
#
|
|
186
|
+
# @example Estimate query cost
|
|
187
|
+
# client.explain('SELECT * FROM events', type: :estimate)
|
|
188
|
+
def explain(sql, type: :plan, settings: {})
|
|
189
|
+
explain_keyword = case type
|
|
190
|
+
when :plan then "EXPLAIN"
|
|
191
|
+
when :pipeline then "EXPLAIN PIPELINE"
|
|
192
|
+
when :estimate then "EXPLAIN ESTIMATE"
|
|
193
|
+
when :ast then "EXPLAIN AST"
|
|
194
|
+
when :syntax then "EXPLAIN SYNTAX"
|
|
195
|
+
else
|
|
196
|
+
raise ArgumentError, "Unknown explain type: #{type}. Valid types: :plan, :pipeline, :estimate, :ast, :syntax"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
explain_sql = "#{explain_keyword} #{sql.strip}"
|
|
200
|
+
execute(explain_sql, settings: settings).to_a
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns detailed health status of the client and server
|
|
204
|
+
#
|
|
205
|
+
# Provides comprehensive health information including server status,
|
|
206
|
+
# connection pool state, and server metrics.
|
|
207
|
+
#
|
|
208
|
+
# @return [Hash] health status
|
|
209
|
+
#
|
|
210
|
+
# @example
|
|
211
|
+
# client.health_check
|
|
212
|
+
# # => {
|
|
213
|
+
# # status: :healthy,
|
|
214
|
+
# # server_reachable: true,
|
|
215
|
+
# # server_version: "24.1.0",
|
|
216
|
+
# # pool: { available: 3, in_use: 2, total: 5 },
|
|
217
|
+
# # uptime_seconds: 3600
|
|
218
|
+
# # }
|
|
219
|
+
def health_check
|
|
220
|
+
started_at = Instrumentation.monotonic_time
|
|
221
|
+
server_reachable = ping
|
|
222
|
+
version = nil
|
|
223
|
+
server_metrics = {}
|
|
224
|
+
|
|
225
|
+
if server_reachable
|
|
226
|
+
begin
|
|
227
|
+
version = server_version
|
|
228
|
+
|
|
229
|
+
# Get server uptime and metrics
|
|
230
|
+
metrics_result = execute(<<~SQL, settings: { max_execution_time: 5 })
|
|
231
|
+
SELECT
|
|
232
|
+
uptime() AS uptime_seconds,
|
|
233
|
+
currentDatabase() AS current_database
|
|
234
|
+
SQL
|
|
235
|
+
server_metrics = metrics_result.first if metrics_result.any?
|
|
236
|
+
rescue StandardError
|
|
237
|
+
# Ignore errors fetching extended info
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
pool_health = @pool.health_check
|
|
242
|
+
check_duration_ms = Instrumentation.duration_ms(started_at)
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
status: server_reachable ? :healthy : :unhealthy,
|
|
246
|
+
server_reachable: server_reachable,
|
|
247
|
+
server_version: version,
|
|
248
|
+
current_database: server_metrics["current_database"],
|
|
249
|
+
server_uptime_seconds: server_metrics["uptime_seconds"],
|
|
250
|
+
pool: pool_health,
|
|
251
|
+
check_duration_ms: check_duration_ms.round(2),
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
170
255
|
# Closes all connections in the pool
|
|
171
256
|
#
|
|
172
257
|
# Call this when shutting down to clean up resources.
|
|
@@ -258,6 +343,9 @@ module ClickhouseRuby
|
|
|
258
343
|
# @param format [String] response format (default: JSONCompact)
|
|
259
344
|
# @return [Result] query results
|
|
260
345
|
def execute_internal(sql, settings: {}, format: DEFAULT_FORMAT)
|
|
346
|
+
started_at = Instrumentation.monotonic_time
|
|
347
|
+
row_count = 0
|
|
348
|
+
|
|
261
349
|
# Build the query with format
|
|
262
350
|
query_with_format = "#{sql.strip} FORMAT #{format}"
|
|
263
351
|
|
|
@@ -268,7 +356,16 @@ module ClickhouseRuby
|
|
|
268
356
|
response = execute_request(query_with_format, params)
|
|
269
357
|
|
|
270
358
|
# Parse response based on format
|
|
271
|
-
parse_response(response, sql, format)
|
|
359
|
+
result = parse_response(response, sql, format)
|
|
360
|
+
row_count = result.size
|
|
361
|
+
|
|
362
|
+
# Instrument successful query
|
|
363
|
+
instrument_query_complete(sql, settings, started_at, row_count)
|
|
364
|
+
|
|
365
|
+
result
|
|
366
|
+
rescue StandardError => e
|
|
367
|
+
instrument_query_error(sql, settings, started_at, e)
|
|
368
|
+
raise
|
|
272
369
|
end
|
|
273
370
|
|
|
274
371
|
# Internal insert without retry wrapper
|
|
@@ -280,6 +377,9 @@ module ClickhouseRuby
|
|
|
280
377
|
# @param format [Symbol] insert format
|
|
281
378
|
# @return [Boolean] true if successful
|
|
282
379
|
def insert_internal(table, rows, columns: nil, settings: {}, format: :json_each_row)
|
|
380
|
+
started_at = Instrumentation.monotonic_time
|
|
381
|
+
row_count = rows.size
|
|
382
|
+
|
|
283
383
|
# Determine columns from first row if not specified
|
|
284
384
|
columns ||= rows.first.keys.map(&:to_s)
|
|
285
385
|
|
|
@@ -312,7 +412,13 @@ module ClickhouseRuby
|
|
|
312
412
|
handle_response(response, sql)
|
|
313
413
|
end
|
|
314
414
|
|
|
415
|
+
# Instrument successful insert
|
|
416
|
+
instrument_insert_complete(table, row_count, settings, started_at)
|
|
417
|
+
|
|
315
418
|
true
|
|
419
|
+
rescue StandardError => e
|
|
420
|
+
instrument_insert_error(table, row_count, settings, started_at, e)
|
|
421
|
+
raise
|
|
316
422
|
end
|
|
317
423
|
|
|
318
424
|
# Builds query parameters including database and settings
|
|
@@ -574,5 +680,110 @@ module ClickhouseRuby
|
|
|
574
680
|
"(code: #{code || "unknown"}, http: #{http_status}, sql: #{truncate_sql(sql, 200)})",
|
|
575
681
|
)
|
|
576
682
|
end
|
|
683
|
+
|
|
684
|
+
# ========================================================================
|
|
685
|
+
# Instrumentation helpers
|
|
686
|
+
# ========================================================================
|
|
687
|
+
|
|
688
|
+
# Instruments a successful query completion
|
|
689
|
+
#
|
|
690
|
+
# @param sql [String] the SQL query
|
|
691
|
+
# @param settings [Hash] query settings
|
|
692
|
+
# @param started_at [Float] monotonic start time
|
|
693
|
+
# @param row_count [Integer] number of rows returned
|
|
694
|
+
def instrument_query_complete(sql, settings, started_at, row_count)
|
|
695
|
+
duration_ms = Instrumentation.duration_ms(started_at)
|
|
696
|
+
|
|
697
|
+
payload = {
|
|
698
|
+
sql: truncate_sql(sql, 500),
|
|
699
|
+
settings: settings,
|
|
700
|
+
row_count: row_count,
|
|
701
|
+
duration_ms: duration_ms,
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
Instrumentation.publish(Instrumentation::EVENTS[:query_complete], payload)
|
|
705
|
+
log_query_timing(sql, duration_ms) if @logger && @config.log_level == :debug
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Instruments a query error
|
|
709
|
+
#
|
|
710
|
+
# @param sql [String] the SQL query
|
|
711
|
+
# @param settings [Hash] query settings
|
|
712
|
+
# @param started_at [Float] monotonic start time
|
|
713
|
+
# @param error [StandardError] the error
|
|
714
|
+
def instrument_query_error(sql, settings, started_at, error)
|
|
715
|
+
duration_ms = Instrumentation.duration_ms(started_at)
|
|
716
|
+
|
|
717
|
+
payload = {
|
|
718
|
+
sql: truncate_sql(sql, 500),
|
|
719
|
+
settings: settings,
|
|
720
|
+
duration_ms: duration_ms,
|
|
721
|
+
exception: [error.class.name, error.message],
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
Instrumentation.publish(Instrumentation::EVENTS[:query_error], payload)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Instruments a successful insert completion
|
|
728
|
+
#
|
|
729
|
+
# @param table [String] the table name
|
|
730
|
+
# @param row_count [Integer] number of rows inserted
|
|
731
|
+
# @param settings [Hash] query settings
|
|
732
|
+
# @param started_at [Float] monotonic start time
|
|
733
|
+
def instrument_insert_complete(table, row_count, settings, started_at)
|
|
734
|
+
duration_ms = Instrumentation.duration_ms(started_at)
|
|
735
|
+
|
|
736
|
+
payload = {
|
|
737
|
+
table: table,
|
|
738
|
+
row_count: row_count,
|
|
739
|
+
settings: settings,
|
|
740
|
+
duration_ms: duration_ms,
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
Instrumentation.publish(Instrumentation::EVENTS[:insert_complete], payload)
|
|
744
|
+
log_insert_timing(table, row_count, duration_ms) if @logger && @config.log_level == :debug
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Instruments an insert error
|
|
748
|
+
#
|
|
749
|
+
# @param table [String] the table name
|
|
750
|
+
# @param row_count [Integer] number of rows attempted
|
|
751
|
+
# @param settings [Hash] query settings
|
|
752
|
+
# @param started_at [Float] monotonic start time
|
|
753
|
+
# @param error [StandardError] the error
|
|
754
|
+
def instrument_insert_error(table, row_count, settings, started_at, error)
|
|
755
|
+
duration_ms = Instrumentation.duration_ms(started_at)
|
|
756
|
+
|
|
757
|
+
payload = {
|
|
758
|
+
table: table,
|
|
759
|
+
row_count: row_count,
|
|
760
|
+
settings: settings,
|
|
761
|
+
duration_ms: duration_ms,
|
|
762
|
+
exception: [error.class.name, error.message],
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
Instrumentation.publish(Instrumentation::EVENTS[:insert_complete], payload)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Logs query timing at debug level
|
|
769
|
+
#
|
|
770
|
+
# @param sql [String] the SQL query
|
|
771
|
+
# @param duration_ms [Float] duration in milliseconds
|
|
772
|
+
def log_query_timing(sql, duration_ms)
|
|
773
|
+
@logger.debug(
|
|
774
|
+
"[ClickhouseRuby] Query completed in #{duration_ms.round(2)}ms: #{truncate_sql(sql, 100)}",
|
|
775
|
+
)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Logs insert timing at debug level
|
|
779
|
+
#
|
|
780
|
+
# @param table [String] the table name
|
|
781
|
+
# @param row_count [Integer] number of rows
|
|
782
|
+
# @param duration_ms [Float] duration in milliseconds
|
|
783
|
+
def log_insert_timing(table, row_count, duration_ms)
|
|
784
|
+
@logger.debug(
|
|
785
|
+
"[ClickhouseRuby] Insert #{row_count} rows into #{table} in #{duration_ms.round(2)}ms",
|
|
786
|
+
)
|
|
787
|
+
end
|
|
577
788
|
end
|
|
578
789
|
end
|
|
@@ -69,11 +69,26 @@ module ClickhouseRuby
|
|
|
69
69
|
# @return [Object] the block's return value
|
|
70
70
|
# @raise [PoolTimeout] if no connection becomes available
|
|
71
71
|
def with_connection
|
|
72
|
+
started_at = Instrumentation.monotonic_time
|
|
72
73
|
conn = checkout
|
|
74
|
+
wait_time_ms = Instrumentation.duration_ms(started_at)
|
|
75
|
+
|
|
76
|
+
# Publish pool checkout event
|
|
77
|
+
Instrumentation.publish(Instrumentation::EVENTS[:pool_checkout], {
|
|
78
|
+
pool_id: object_id,
|
|
79
|
+
wait_time_ms: wait_time_ms,
|
|
80
|
+
connection_id: conn.object_id,
|
|
81
|
+
},)
|
|
82
|
+
|
|
73
83
|
begin
|
|
74
84
|
yield conn
|
|
75
85
|
ensure
|
|
76
86
|
checkin(conn)
|
|
87
|
+
# Publish pool checkin event
|
|
88
|
+
Instrumentation.publish(Instrumentation::EVENTS[:pool_checkin], {
|
|
89
|
+
pool_id: object_id,
|
|
90
|
+
connection_id: conn.object_id,
|
|
91
|
+
},)
|
|
77
92
|
end
|
|
78
93
|
end
|
|
79
94
|
|
|
@@ -109,6 +124,13 @@ module ClickhouseRuby
|
|
|
109
124
|
remaining = deadline - Time.now
|
|
110
125
|
if remaining <= 0
|
|
111
126
|
@total_timeouts += 1
|
|
127
|
+
# Publish pool timeout event
|
|
128
|
+
Instrumentation.publish(Instrumentation::EVENTS[:pool_timeout], {
|
|
129
|
+
pool_id: object_id,
|
|
130
|
+
wait_time_ms: @timeout * 1000,
|
|
131
|
+
pool_size: @size,
|
|
132
|
+
in_use: @in_use.size,
|
|
133
|
+
},)
|
|
112
134
|
raise PoolTimeout, "Could not obtain a connection from the pool within #{@timeout} seconds " \
|
|
113
135
|
"(pool size: #{@size}, in use: #{@in_use.size})"
|
|
114
136
|
end
|
|
@@ -250,7 +272,57 @@ module ClickhouseRuby
|
|
|
250
272
|
total_connections: @all_connections.size,
|
|
251
273
|
total_checkouts: @total_checkouts,
|
|
252
274
|
total_timeouts: @total_timeouts,
|
|
253
|
-
uptime_seconds: Time.now - @created_at,
|
|
275
|
+
uptime_seconds: (Time.now - @created_at).round(2),
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Returns detailed pool statistics for monitoring
|
|
281
|
+
#
|
|
282
|
+
# Provides comprehensive metrics suitable for Prometheus/StatsD export.
|
|
283
|
+
#
|
|
284
|
+
# @return [Hash] detailed pool statistics
|
|
285
|
+
#
|
|
286
|
+
# @example
|
|
287
|
+
# pool.detailed_stats
|
|
288
|
+
# # => {
|
|
289
|
+
# # capacity: 5,
|
|
290
|
+
# # connections: { total: 5, available: 3, in_use: 2 },
|
|
291
|
+
# # utilization_percent: 40.0,
|
|
292
|
+
# # checkouts: { total: 1000, rate_per_minute: 16.7 },
|
|
293
|
+
# # timeouts: { total: 5, rate_per_minute: 0.08 },
|
|
294
|
+
# # uptime_seconds: 3600
|
|
295
|
+
# # }
|
|
296
|
+
def detailed_stats
|
|
297
|
+
@mutex.synchronize do
|
|
298
|
+
uptime = Time.now - @created_at
|
|
299
|
+
uptime_minutes = uptime / 60.0
|
|
300
|
+
|
|
301
|
+
in_use = @in_use.size
|
|
302
|
+
available = @available.size
|
|
303
|
+
total = @all_connections.size
|
|
304
|
+
utilization = total.positive? ? (in_use.to_f / total * 100).round(2) : 0.0
|
|
305
|
+
|
|
306
|
+
checkout_rate = uptime_minutes.positive? ? (@total_checkouts / uptime_minutes).round(2) : 0.0
|
|
307
|
+
timeout_rate = uptime_minutes.positive? ? (@total_timeouts / uptime_minutes).round(4) : 0.0
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
capacity: @size,
|
|
311
|
+
connections: {
|
|
312
|
+
total: total,
|
|
313
|
+
available: available,
|
|
314
|
+
in_use: in_use,
|
|
315
|
+
},
|
|
316
|
+
utilization_percent: utilization,
|
|
317
|
+
checkouts: {
|
|
318
|
+
total: @total_checkouts,
|
|
319
|
+
rate_per_minute: checkout_rate,
|
|
320
|
+
},
|
|
321
|
+
timeouts: {
|
|
322
|
+
total: @total_timeouts,
|
|
323
|
+
rate_per_minute: timeout_rate,
|
|
324
|
+
},
|
|
325
|
+
uptime_seconds: uptime.round(2),
|
|
254
326
|
}
|
|
255
327
|
end
|
|
256
328
|
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
# Instrumentation module for observability and monitoring
|
|
5
|
+
#
|
|
6
|
+
# Provides ActiveSupport::Notifications integration when available,
|
|
7
|
+
# with graceful fallback for non-Rails applications.
|
|
8
|
+
#
|
|
9
|
+
# @example Subscribe to events (with ActiveSupport)
|
|
10
|
+
# ActiveSupport::Notifications.subscribe(/clickhouse_ruby/) do |name, start, finish, id, payload|
|
|
11
|
+
# duration = finish - start
|
|
12
|
+
# Rails.logger.info "#{name} took #{duration}s"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Subscribe to specific events
|
|
16
|
+
# ActiveSupport::Notifications.subscribe('clickhouse_ruby.query.complete') do |*args|
|
|
17
|
+
# event = ActiveSupport::Notifications::Event.new(*args)
|
|
18
|
+
# puts "Query: #{event.payload[:sql]} took #{event.duration}ms"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module Instrumentation
|
|
22
|
+
# Event names for instrumentation
|
|
23
|
+
EVENTS = {
|
|
24
|
+
query_start: "clickhouse_ruby.query.start",
|
|
25
|
+
query_complete: "clickhouse_ruby.query.complete",
|
|
26
|
+
query_error: "clickhouse_ruby.query.error",
|
|
27
|
+
insert_start: "clickhouse_ruby.insert.start",
|
|
28
|
+
insert_complete: "clickhouse_ruby.insert.complete",
|
|
29
|
+
pool_checkout: "clickhouse_ruby.pool.checkout",
|
|
30
|
+
pool_checkin: "clickhouse_ruby.pool.checkin",
|
|
31
|
+
pool_timeout: "clickhouse_ruby.pool.timeout",
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Check if ActiveSupport::Notifications is available
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean] true if ActiveSupport::Notifications is available
|
|
38
|
+
def available?
|
|
39
|
+
defined?(ActiveSupport::Notifications)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Instrument a block of code with timing and event notification
|
|
43
|
+
#
|
|
44
|
+
# When ActiveSupport::Notifications is available, publishes an event
|
|
45
|
+
# with the given name and payload. Otherwise, still tracks timing
|
|
46
|
+
# for logging purposes.
|
|
47
|
+
#
|
|
48
|
+
# @param event_name [String] the event name to publish
|
|
49
|
+
# @param payload [Hash] additional payload data
|
|
50
|
+
# @yield the block to instrument
|
|
51
|
+
# @return [Object] the result of the block
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# Instrumentation.instrument('clickhouse_ruby.query.complete', sql: 'SELECT 1') do
|
|
55
|
+
# execute_query
|
|
56
|
+
# end
|
|
57
|
+
def instrument(event_name, payload = {})
|
|
58
|
+
if available?
|
|
59
|
+
ActiveSupport::Notifications.instrument(event_name, payload) { yield }
|
|
60
|
+
else
|
|
61
|
+
instrument_without_as(event_name, payload) { yield }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Publish an event without a block (for start/error events)
|
|
66
|
+
#
|
|
67
|
+
# @param event_name [String] the event name to publish
|
|
68
|
+
# @param payload [Hash] the event payload
|
|
69
|
+
# @return [void]
|
|
70
|
+
def publish(event_name, payload = {})
|
|
71
|
+
return unless available?
|
|
72
|
+
|
|
73
|
+
ActiveSupport::Notifications.publish(event_name, payload)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns a monotonic timestamp for duration calculation
|
|
77
|
+
#
|
|
78
|
+
# Uses Process.clock_gettime for accurate timing that isn't
|
|
79
|
+
# affected by system clock changes.
|
|
80
|
+
#
|
|
81
|
+
# @return [Float] monotonic timestamp in seconds
|
|
82
|
+
def monotonic_time
|
|
83
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Calculate duration in milliseconds from a start time
|
|
87
|
+
#
|
|
88
|
+
# @param started_at [Float] monotonic start time
|
|
89
|
+
# @return [Float] duration in milliseconds
|
|
90
|
+
def duration_ms(started_at)
|
|
91
|
+
(monotonic_time - started_at) * 1000
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Fallback instrumentation when ActiveSupport is not available
|
|
97
|
+
#
|
|
98
|
+
# Still tracks timing so it can be used for logging.
|
|
99
|
+
#
|
|
100
|
+
# @param event_name [String] the event name
|
|
101
|
+
# @param payload [Hash] the payload
|
|
102
|
+
# @yield the block to execute
|
|
103
|
+
# @return [Object] the result of the block
|
|
104
|
+
def instrument_without_as(_event_name, payload)
|
|
105
|
+
started_at = monotonic_time
|
|
106
|
+
result = yield
|
|
107
|
+
payload[:duration_ms] = duration_ms(started_at)
|
|
108
|
+
result
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
payload[:duration_ms] = duration_ms(started_at)
|
|
111
|
+
payload[:exception] = [e.class.name, e.message]
|
|
112
|
+
raise
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Helper module for including instrumentation in classes
|
|
117
|
+
module Helpers
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Instrument a query operation
|
|
121
|
+
#
|
|
122
|
+
# @param sql [String] the SQL query
|
|
123
|
+
# @param settings [Hash] query settings
|
|
124
|
+
# @yield the block to execute
|
|
125
|
+
# @return [Object] the result
|
|
126
|
+
def instrument_query(sql, settings: {})
|
|
127
|
+
payload = {
|
|
128
|
+
sql: sql,
|
|
129
|
+
settings: settings,
|
|
130
|
+
connection_id: object_id,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Instrumentation.instrument(EVENTS[:query_complete], payload) { yield }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Instrument an insert operation
|
|
137
|
+
#
|
|
138
|
+
# @param table [String] the table name
|
|
139
|
+
# @param row_count [Integer] number of rows
|
|
140
|
+
# @param settings [Hash] query settings
|
|
141
|
+
# @yield the block to execute
|
|
142
|
+
# @return [Object] the result
|
|
143
|
+
def instrument_insert(table, row_count:, settings: {})
|
|
144
|
+
payload = {
|
|
145
|
+
table: table,
|
|
146
|
+
row_count: row_count,
|
|
147
|
+
settings: settings,
|
|
148
|
+
connection_id: object_id,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Instrumentation.instrument(EVENTS[:insert_complete], payload) { yield }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Instrument a pool checkout operation
|
|
155
|
+
#
|
|
156
|
+
# @yield the block to execute
|
|
157
|
+
# @return [Object] the result
|
|
158
|
+
def instrument_pool_checkout
|
|
159
|
+
payload = { pool_id: object_id }
|
|
160
|
+
|
|
161
|
+
Instrumentation.instrument(EVENTS[:pool_checkout], payload) { yield }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Publish a pool timeout event
|
|
165
|
+
#
|
|
166
|
+
# @param wait_time [Float] how long we waited before timeout
|
|
167
|
+
# @return [void]
|
|
168
|
+
def publish_pool_timeout(wait_time:)
|
|
169
|
+
Instrumentation.publish(EVENTS[:pool_timeout], {
|
|
170
|
+
pool_id: object_id,
|
|
171
|
+
wait_time_ms: wait_time * 1000,
|
|
172
|
+
},)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/clickhouse_ruby.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "clickhouse_ruby/version"
|
|
4
4
|
require_relative "clickhouse_ruby/errors"
|
|
5
5
|
require_relative "clickhouse_ruby/configuration"
|
|
6
|
+
require_relative "clickhouse_ruby/instrumentation"
|
|
6
7
|
require_relative "clickhouse_ruby/types"
|
|
7
8
|
require_relative "clickhouse_ruby/result"
|
|
8
9
|
require_relative "clickhouse_ruby/retry_handler"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clickhouse-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mohamad Kamar
|
|
@@ -108,6 +108,20 @@ dependencies:
|
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '2.20'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: benchmark-ips
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '2.12'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '2.12'
|
|
111
125
|
description: A lightweight Ruby client for ClickHouse with optional ActiveRecord integration.
|
|
112
126
|
Provides a simple interface for querying, inserting, and managing ClickHouse databases.
|
|
113
127
|
email:
|
|
@@ -123,14 +137,19 @@ files:
|
|
|
123
137
|
- lib/clickhouse_ruby/active_record.rb
|
|
124
138
|
- lib/clickhouse_ruby/active_record/arel_visitor.rb
|
|
125
139
|
- lib/clickhouse_ruby/active_record/connection_adapter.rb
|
|
140
|
+
- lib/clickhouse_ruby/active_record/generators/migration_generator.rb
|
|
141
|
+
- lib/clickhouse_ruby/active_record/generators/templates/create_table.rb.tt
|
|
142
|
+
- lib/clickhouse_ruby/active_record/generators/templates/migration.rb.tt
|
|
126
143
|
- lib/clickhouse_ruby/active_record/railtie.rb
|
|
127
144
|
- lib/clickhouse_ruby/active_record/relation_extensions.rb
|
|
145
|
+
- lib/clickhouse_ruby/active_record/schema_dumper.rb
|
|
128
146
|
- lib/clickhouse_ruby/active_record/schema_statements.rb
|
|
129
147
|
- lib/clickhouse_ruby/client.rb
|
|
130
148
|
- lib/clickhouse_ruby/configuration.rb
|
|
131
149
|
- lib/clickhouse_ruby/connection.rb
|
|
132
150
|
- lib/clickhouse_ruby/connection_pool.rb
|
|
133
151
|
- lib/clickhouse_ruby/errors.rb
|
|
152
|
+
- lib/clickhouse_ruby/instrumentation.rb
|
|
134
153
|
- lib/clickhouse_ruby/result.rb
|
|
135
154
|
- lib/clickhouse_ruby/retry_handler.rb
|
|
136
155
|
- lib/clickhouse_ruby/streaming_result.rb
|