class NewRelic::Agent::SqlSampler

This class contains the logic of recording slow SQL traces, which may represent multiple aggregated SQL queries.

A slow SQL trace consists of a collection of SQL instrumented SQL queries that all normalize to the same text. For example, the following two queries would be aggregated together into a single slow SQL trace:

SELECT * FROM table WHERE id=42
SELECT * FROM table WHERE id=1234

Each slow SQL trace keeps track of the number of times the same normalized query was seen, the min, max, and total time spent executing those queries, and an example backtrace from one of the aggregated queries.

@api public

Constants

MAX_SAMPLES
PRIORITY

Attributes

disabled[R]
sql_traces[R]

this is for unit tests only

Public Class Methods

new() click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 33
def initialize
  @sql_traces = {}

  # This lock is used to synchronize access to @sql_traces
  # and related variables. It can become necessary on JRuby or
  # any 'honest-to-god'-multithreaded system
  @samples_lock = Mutex.new
end

Public Instance Methods

distributed_trace_attributes(state) click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 158
def distributed_trace_attributes(state)
  transaction = state.current_transaction
  params = nil
  if transaction&.distributed_tracer&.distributed_trace_payload
    params = {}
    payload = transaction.distributed_tracer.distributed_trace_payload
    DistributedTraceAttributes.copy_from_transaction(transaction, payload, params)
    params[PRIORITY] = transaction.priority
  end
  params
end
enabled?() click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 42
def enabled?
  Agent.config[:'slow_sql.enabled'] &&
    Agent.config[:'transaction_tracer.enabled'] &&
    NewRelic::Agent::Database.should_record_sql?(:slow_sql)
end
harvest!() click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 197
def harvest!
  return NewRelic::EMPTY_ARRAY unless enabled?

  slowest = []
  @samples_lock.synchronize do
    slowest = @sql_traces.values
    @sql_traces = {}
  end
  slowest.each { |trace| trace.prepare_to_send }
  slowest
end
has_room?() click to toggle source

this should always be called under the @samples_lock

# File lib/new_relic/agent/sql_sampler.rb, line 115
def has_room?
  @sql_traces.size < MAX_SAMPLES
end
merge!(sql_traces) click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 184
def merge!(sql_traces)
  @samples_lock.synchronize do
    sql_traces.each do |trace|
      existing_trace = @sql_traces[trace.sql]
      if existing_trace
        existing_trace.aggregate_trace(trace)
      else
        @sql_traces[trace.sql] = trace
      end
    end
  end
end
notice_sql(sql, metric_name, config, duration, state = nil, explainer = nil, binds = nil, name = nil) click to toggle source

Records an SQL query, potentially creating a new slow SQL trace, or aggregating the query into an existing slow SQL trace.

This method should be used only by gem authors wishing to extend the Ruby agent to instrument new database interfaces - it should generally not be called directly from application code.

@param sql [String] the SQL query being recorded @param metric_name [String] is the metric name under which this query will be recorded @param config [Object] is the driver configuration for the connection @param duration [Float] number of seconds the query took to execute @param explainer [Proc] for internal use only - 3rd-party clients must

not pass this parameter.

@api public @deprecated Use {Datastores.notice_sql} instead.

# File lib/new_relic/agent/sql_sampler.rb, line 142
def notice_sql(sql, metric_name, config, duration, state = nil, explainer = nil, binds = nil, name = nil) # THREAD_LOCAL_ACCESS sometimes
  state ||= Tracer.state
  data = state.sql_sampler_transaction_data
  return unless data

  if state.is_sql_recorded?
    if duration > Agent.config[:'slow_sql.explain_threshold']
      backtrace = caller.join("\n")
      statement = Database::Statement.new(sql, config, explainer, binds, name)
      data.sql_data << SlowSql.new(statement, metric_name, duration, backtrace)
    end
  end
end
notice_sql_statement(statement, metric_name, duration) click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 170
def notice_sql_statement(statement, metric_name, duration)
  state ||= Tracer.state
  data = state.sql_sampler_transaction_data
  return unless data

  if state.is_sql_recorded?
    if duration > Agent.config[:'slow_sql.explain_threshold']
      backtrace = caller.join("\n")
      params = distributed_trace_attributes(state)
      data.sql_data << SlowSql.new(statement, metric_name, duration, backtrace, params)
    end
  end
end
on_finishing_transaction(state, name) click to toggle source

This is called when we are done with the transaction.

# File lib/new_relic/agent/sql_sampler.rb, line 67
def on_finishing_transaction(state, name)
  return unless enabled?

  data = state.sql_sampler_transaction_data
  return unless data

  data.set_transaction_name(name)
  if data.sql_data.size > 0
    @samples_lock.synchronize do
      ::NewRelic::Agent.logger.debug("Examining #{data.sql_data.size} slow transaction sql statement(s)")
      save_slow_sql(data)
    end
  end
end
on_start_transaction(state, uri = nil) click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 48
def on_start_transaction(state, uri = nil)
  return unless enabled?

  state.sql_sampler_transaction_data = TransactionSqlData.new

  if state.current_transaction
    guid = state.current_transaction.guid
  end

  if Agent.config[:'slow_sql.enabled'] && state.sql_sampler_transaction_data
    state.sql_sampler_transaction_data.set_transaction_info(uri, guid)
  end
end
remove_shortest_trace() click to toggle source

this should always be called under the @samples_lock

# File lib/new_relic/agent/sql_sampler.rb, line 120
def remove_shortest_trace
  shortest_key, _ = @sql_traces.min_by { |(_, trace)| trace.max_call_time }
  @sql_traces.delete(shortest_key)
end
reset!() click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 209
def reset!
  @samples_lock.synchronize do
    @sql_traces = {}
  end
end
save_slow_sql(transaction_sql_data) click to toggle source

this should always be called under the @samples_lock

# File lib/new_relic/agent/sql_sampler.rb, line 83
def save_slow_sql(transaction_sql_data)
  path = transaction_sql_data.path
  uri = transaction_sql_data.uri

  transaction_sql_data.sql_data.each do |sql_item|
    normalized_sql = sql_item.normalize
    sql_trace = @sql_traces[normalized_sql]
    if sql_trace
      sql_trace.aggregate(sql_item, path, uri)
    else
      if has_room?
        sql_trace = SqlTrace.new(normalized_sql, sql_item, path, uri)
      elsif should_add_trace?(sql_item)
        remove_shortest_trace
        sql_trace = SqlTrace.new(normalized_sql, sql_item, path, uri)
      end

      if sql_trace
        @sql_traces[normalized_sql] = sql_trace
      end
    end
  end
end
should_add_trace?(sql_item) click to toggle source

this should always be called under the @samples_lock

# File lib/new_relic/agent/sql_sampler.rb, line 108
def should_add_trace?(sql_item)
  @sql_traces.any? do |(_, existing_trace)|
    existing_trace.max_call_time < sql_item.duration
  end
end
tl_transaction_data() click to toggle source
# File lib/new_relic/agent/sql_sampler.rb, line 62
def tl_transaction_data # only used for testing
  Tracer.state.sql_sampler_transaction_data
end