Class: NewRelic::Agent::SqlSampler

Inherits:
Object
  • Object
show all
Defined in:
lib/new_relic/agent/sql_sampler.rb

Overview

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.

Constant Summary collapse

MAX_SAMPLES =
10
PRIORITY =
'priority'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSqlSampler

Returns a new instance of SqlSampler.



34
35
36
37
38
39
40
41
# File 'lib/new_relic/agent/sql_sampler.rb', line 34

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

Instance Attribute Details

#disabledObject (readonly)



27
28
29
# File 'lib/new_relic/agent/sql_sampler.rb', line 27

def disabled
  @disabled
end

#sql_tracesObject (readonly)

this is for unit tests only



30
31
32
# File 'lib/new_relic/agent/sql_sampler.rb', line 30

def sql_traces
  @sql_traces
end

Instance Method Details

#distributed_trace_attributes(state) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/new_relic/agent/sql_sampler.rb', line 159

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?Boolean

Returns:

  • (Boolean)


43
44
45
46
47
# File 'lib/new_relic/agent/sql_sampler.rb', line 43

def enabled?
  Agent.config[:'slow_sql.enabled'] &&
    Agent.config[:'transaction_tracer.enabled'] &&
    NewRelic::Agent::Database.should_record_sql?(:slow_sql)
end

#harvest!Object



198
199
200
201
202
203
204
205
206
207
208
# File 'lib/new_relic/agent/sql_sampler.rb', line 198

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?Boolean

this should always be called under the @samples_lock

Returns:

  • (Boolean)


116
117
118
# File 'lib/new_relic/agent/sql_sampler.rb', line 116

def has_room?
  @sql_traces.size < MAX_SAMPLES
end

#merge!(sql_traces) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/new_relic/agent/sql_sampler.rb', line 185

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) ⇒ Object

Deprecated.

Use Datastores.notice_sql instead.

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.

Parameters:

  • sql (String)

    the SQL query being recorded

  • metric_name (String)

    is the metric name under which this query will be recorded

  • config (Object)

    is the driver configuration for the connection

  • duration (Float)

    number of seconds the query took to execute

  • explainer (Proc) (defaults to: nil)

    for internal use only - 3rd-party clients must not pass this parameter.



143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/new_relic/agent/sql_sampler.rb', line 143

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) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/new_relic/agent/sql_sampler.rb', line 171

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) ⇒ Object

This is called when we are done with the transaction.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/new_relic/agent/sql_sampler.rb', line 68

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) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/new_relic/agent/sql_sampler.rb', line 49

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_traceObject

this should always be called under the @samples_lock



121
122
123
124
# File 'lib/new_relic/agent/sql_sampler.rb', line 121

def remove_shortest_trace
  shortest_key, _ = @sql_traces.min_by { |(_, trace)| trace.max_call_time }
  @sql_traces.delete(shortest_key)
end

#reset!Object



210
211
212
213
214
# File 'lib/new_relic/agent/sql_sampler.rb', line 210

def reset!
  @samples_lock.synchronize do
    @sql_traces = {}
  end
end

#save_slow_sql(transaction_sql_data) ⇒ Object

this should always be called under the @samples_lock



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/new_relic/agent/sql_sampler.rb', line 84

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) ⇒ Boolean

this should always be called under the @samples_lock

Returns:

  • (Boolean)


109
110
111
112
113
# File 'lib/new_relic/agent/sql_sampler.rb', line 109

def should_add_trace?(sql_item)
  @sql_traces.any? do |(_, existing_trace)|
    existing_trace.max_call_time < sql_item.duration
  end
end

#tl_transaction_dataObject

only used for testing



63
64
65
# File 'lib/new_relic/agent/sql_sampler.rb', line 63

def tl_transaction_data # only used for testing
  Tracer.state.sql_sampler_transaction_data
end