Module: Ditliti::DbTracing

Defined in:
lib/ditliti/db.rb

Overview

Database query auto-instrumentation.

Wraps a query execution with a db.query span carrying OpenTelemetry- shaped database attributes. The backend (ingestion-api) parameterizes and sanitizes db.query.text server-side and derives db.query.summary / db.operation.name / db.collection.name / ditliti.query_hash from it - callers here only need to supply the real SQL text, exactly like every other span already sent by this SDK travels over the same trust boundary to the user's own ingestion-api.

This never changes the wrapped call's exception flow: span-recording failures are swallowed, and the original exception (if any) is always re-raised unchanged.

Note: this environment has no Ruby runtime available, so this file has been carefully written (mirroring the already-tested Python/PHP/Elixir adapters' generic-wrapper + ORM-notification shape) but has not itself been executed - review before relying on it in production.

Class Method Summary collapse

Class Method Details

.extract_row_count(result) ⇒ Object



92
93
94
95
96
97
# File 'lib/ditliti/db.rb', line 92

def extract_row_count(result)
  return result if result.is_a?(Integer) && result >= 0
  return result.length if result.respond_to?(:length)

  nil
end

.record_completed_span(client, system:, text:, duration_ms:, collection: nil, operation: nil, error_type: nil) ⇒ Object

Records a span whose duration is already known (e.g. from ActiveRecord's notification, which reports a query's elapsed time only after it has already completed) - unlike trace_query, there's no separate "finish" step since the operation is already over.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ditliti/db.rb', line 66

def record_completed_span(client, system:, text:, duration_ms:, collection: nil, operation: nil, error_type: nil)
  client.start_trace if client.trace.nil?
  parent_span_id = client.trace.span_id
  ctx = client.trace.child

  tags = { "db.system.name" => system, "db.query.text" => text }
  tags["db.operation.name"] = operation if operation
  tags["db.collection.name"] = collection if collection
  tags["error.type"] = error_type if error_type

  client.record_span(
    trace_id: ctx.trace_id,
    span_id: ctx.span_id,
    parent_span_id: parent_span_id,
    operation_name: "db.query",
    duration_ms: duration_ms,
    tags: tags
  )
end

.safe_finish(span, extra_tags) ⇒ Object



86
87
88
89
90
# File 'lib/ditliti/db.rb', line 86

def safe_finish(span, extra_tags)
  span.finish(extra_tags: extra_tags)
rescue StandardError
  # Instrumentation must never break the caller's query path.
end

.subscribe_active_record(client, system: "postgresql") ⇒ Object

Subscribes to ActiveRecord's sql.active_record notification - no query wrapping required, works for the query builder, Active Record models, and raw connection.execute alike. Requires ActiveSupport to already be loaded by the host application (this SDK has no Rails dependency of its own).



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/ditliti/db.rb', line 47

def subscribe_active_record(client, system: "postgresql")
  ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    next if event.payload[:name] == "SCHEMA" # migrations/introspection queries, not app queries

    record_completed_span(
      client,
      system: system,
      text: event.payload[:sql],
      duration_ms: event.duration,
      collection: event.payload[:name]
    )
  end
end

.trace_query(client, system:, text:, collection: nil, operation: nil) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/ditliti/db.rb', line 25

def trace_query(client, system:, text:, collection: nil, operation: nil)
  tags = { "db.system.name" => system, "db.query.text" => text }
  tags["db.operation.name"] = operation if operation
  tags["db.collection.name"] = collection if collection

  span = client.start_span("db.query", tags: tags)
  begin
    result = yield
    count = extract_row_count(result)
    safe_finish(span, count ? { "db.response.returned_rows" => count.to_s } : {})
    result
  rescue => error
    safe_finish(span, { "error.type" => error.class.name })
    raise
  end
end