Module: RailsOtelContext::Adapters::Clickhouse

Defined in:
lib/rails_otel_context/adapters/clickhouse.rb

Constant Summary collapse

CANDIDATE_METHODS =

click_house gem v1.x used :query/:select; v2.x uses :select_all/:select_one/:select_value. We list all known variants so install! picks whichever the loaded gem version defines.

%i[
  select_all select_one select_value
  insert insert_compact insert_rows
  execute command
  query select
].freeze
REENTRANCY_KEY =
:_rails_otel_ctx_clickhouse_instrumenting
METHOD_OP_ALIAS =

Maps compound gem method names to their SQL verb for span naming. select_all/select_one/select_value → SELECT; insert_* → INSERT.

{
  'SELECT_ALL' => 'SELECT',
  'SELECT_ONE' => 'SELECT',
  'SELECT_VALUE' => 'SELECT',
  'INSERT_COMPACT' => 'INSERT',
  'INSERT_ROWS' => 'INSERT'
}.freeze

Class Method Summary collapse

Class Method Details

.install!(app_root:) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/rails_otel_context/adapters/clickhouse.rb', line 18

def install!(app_root:)
  begin
    require 'click_house'
  rescue LoadError
    # ClickHouse client gem is optional for consumers.
  end

  target_clients.each do |klass|
    methods = CANDIDATE_METHODS.select { |method_name| klass.method_defined?(method_name) }
    next if methods.empty?

    patch_module = patch_module_for(klass, methods)
    patch_module.configure(app_root: app_root)
    next if klass.ancestors.include?(patch_module)

    klass.prepend(patch_module)
  end
end

.parse_table(statement) ⇒ Object

Returns [db_name, table_name] extracted from the SQL statement. db_name is the schema prefix (e.g. “mailgun_analytics”), nil when absent. table_name is the bare table (e.g. “mailgun_events”), nil when not found.



89
90
91
92
93
94
95
96
97
# File 'lib/rails_otel_context/adapters/clickhouse.rb', line 89

def parse_table(statement)
  return [nil, nil] unless statement.is_a?(String)

  qualified = statement.match(/(?:\bFROM\b|\bINTO\b|\bUPDATE\b)\s+([\w.]+)/i)&.captures&.first
  return [nil, nil] unless qualified

  parts = qualified.split('.')
  parts.size > 1 ? [parts[0..-2].join('.'), parts.last] : [nil, parts.first]
end

.patch_module_for(klass, methods) ⇒ Object



47
48
49
50
51
# File 'lib/rails_otel_context/adapters/clickhouse.rb', line 47

def patch_module_for(klass, methods)
  @patch_modules ||= {}
  key = [klass.name, methods.sort].join(':')
  @patch_modules[key] ||= build_patch_module(methods)
end

.span_name_for(statement, method_op, table_name: nil) ⇒ Object

Derives a human-readable span name from the SQL statement. Follows OTel DB convention: “sql_verb table”. Falls back to “method_op clickhouse” when the statement is absent or cannot be parsed (e.g. raw ClickHouse commands with no FROM clause).

Accepts an optional pre-parsed table_name to avoid a second regex scan when the caller already holds the result of parse_table.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/rails_otel_context/adapters/clickhouse.rb', line 70

def span_name_for(statement, method_op, table_name: nil)
  effective_op = METHOD_OP_ALIAS.fetch(method_op, method_op)
  return "#{effective_op} clickhouse" unless statement.is_a?(String)

  sql_verb   = statement.lstrip.split(/\s/, 2).first&.upcase
  table_name = parse_table(statement).last if table_name.nil?

  if sql_verb && table_name
    "#{sql_verb} #{table_name}"
  elsif sql_verb
    "#{sql_verb} clickhouse"
  else
    "#{effective_op} clickhouse"
  end
end

.target_clientsObject



37
38
39
40
41
42
43
44
45
# File 'lib/rails_otel_context/adapters/clickhouse.rb', line 37

def target_clients
  clients = []

  clients << ::ClickHouse::Client if defined?(::ClickHouse::Client)
  clients << ::ClickHouse::Connection if defined?(::ClickHouse::Connection)
  clients << ::Clickhouse::Client if defined?(::Clickhouse::Client)

  clients.uniq
end