module Mmtrix::Agent::Database

Constants

EMPTY_STRING
KNOWN_OPERATIONS
MAX_QUERY_LENGTH
QUERY_PLAN
RECORD_FOR
SQLITE_EXPLAIN_COLUMNS
SQL_COMMENT_REGEX
SUPPORTED_ADAPTERS_FOR_EXPLAIN

Public Instance Methods

adapter_from_config(config) click to toggle source

This takes a connection config hash from ActiveRecord or Sequel and returns a string describing the associated database adapter

# File lib/mmtrix/agent/database.rb, line 100
def adapter_from_config(config)
  if config[:adapter]
    return config[:adapter].to_s
  elsif config[:uri] && config[:uri].to_s =~ /^jdbc:([^:]+):/
    # This case is for Sequel with the jdbc-mysql, jdbc-postgres, or
    # jdbc-sqlite3 gems.
    return $1
  end
end
capture_query(query) click to toggle source
# File lib/mmtrix/agent/database.rb, line 31
def capture_query(query)
  Helper.correctly_encoded(truncate_query(query))
end
close_connections() click to toggle source
# File lib/mmtrix/agent/database.rb, line 94
def close_connections
  ConnectionManager.instance.close_connections
end
explain_sql(sql, connection_config, explainer=nil) click to toggle source

Perform this in the runtime environment of a managed application, to explain the sql statement executed within a node of a transaction sample. Returns an array of explanations (which is an array rows consisting of an array of strings for each column returned by the the explain query) Note this happens only for statements whose execution time exceeds a threshold (e.g. 500ms) and only within the slowest transaction in a report period, selected for shipment to New Mmtrix

# File lib/mmtrix/agent/database.rb, line 119
def explain_sql(sql, connection_config, explainer=nil)
  return nil unless sql && explainer && connection_config
  statement = sql.split(";\n")[0] # only explain the first
  explain_plan = explain_statement(statement, connection_config, explainer)
  return explain_plan || []
end
explain_statement(statement, config, explainer) click to toggle source
# File lib/mmtrix/agent/database.rb, line 128
def explain_statement(statement, config, explainer)
  return unless explainer && is_select?(statement)

  if statement[-3,3] == '...'
    Mmtrix::Agent.logger.debug('Unable to collect explain plan for truncated query.')
    return
  end

  if parameterized?(statement)
    Mmtrix::Agent.logger.debug('Unable to collect explain plan for parameterized query.')
    return
  end

  adapter = adapter_from_config(config)
  if !SUPPORTED_ADAPTERS_FOR_EXPLAIN.include?(adapter)
    Mmtrix::Agent.logger.debug("Not collecting explain plan because an unknown connection adapter ('#{adapter}') was used.")
    return
  end

  handle_exception_in_explain do
    start = Time.now
    plan = explainer.call(config, statement)
    ::Mmtrix::Agent.record_metric("Supportability/Database/execute_explain_plan", Time.now - start)
    return process_resultset(plan, adapter) if plan
  end
end
get_connection(config, &connector) click to toggle source
# File lib/mmtrix/agent/database.rb, line 90
def get_connection(config, &connector)
  ConnectionManager.instance.get_connection(config, &connector)
end
handle_exception_in_explain() { || ... } click to toggle source
# File lib/mmtrix/agent/database.rb, line 238
def handle_exception_in_explain
  yield
rescue => e
  begin
    # guarantees no throw from explain_sql
    ::Mmtrix::Agent.logger.error("Error getting query plan:", e)
    nil
  rescue
    # double exception. throw up your hands
    nil
  end
end
is_select?(statement) click to toggle source
# File lib/mmtrix/agent/database.rb, line 276
def is_select?(statement)
  parse_operation_from_query(statement) == 'select'
end
obfuscate_sql(sql) click to toggle source
# File lib/mmtrix/agent/database.rb, line 43
def obfuscate_sql(sql)
  Obfuscator.instance.obfuscator.call(sql)
end
parameterized?(statement) click to toggle source
# File lib/mmtrix/agent/database.rb, line 280
def parameterized?(statement)
  Obfuscator.instance.obfuscate_single_quote_literals(statement) =~ /\$\d+/
end
parse_operation_from_query(sql) click to toggle source
# File lib/mmtrix/agent/database.rb, line 268
def parse_operation_from_query(sql)
  sql = sql.gsub(SQL_COMMENT_REGEX, EMPTY_STRING)
  if sql =~ /(\w+)/
    op = $1.downcase
    return op if KNOWN_OPERATIONS.include?(op)
  end
end
process_explain_results_mysql(results) click to toggle source
# File lib/mmtrix/agent/database.rb, line 196
def process_explain_results_mysql(results)
  return string_explain_plan_results(results) if results.is_a?(String)
  headers = []
  values  = []
  if results.is_a?(Array)
    # We're probably using the jdbc-mysql gem for JRuby, which will give
    # us an array of hashes.
    headers = results.first.keys
    results.each do |row|
      values << headers.map { |h| row[h] }
    end
  else
    # We're probably using the native mysql driver gem, which will give us
    # a Mysql::Result object that responds to each_hash
    results.each_hash do |row|
      headers = row.keys
      values << headers.map { |h| row[h] }
    end
  end
  [headers, values]
end
process_explain_results_mysql2(results) click to toggle source
# File lib/mmtrix/agent/database.rb, line 218
def process_explain_results_mysql2(results)
  return string_explain_plan_results(results) if results.is_a?(String)
  headers = results.fields
  values  = []
  results.each { |row| values << row }
  [headers, values]
end
process_explain_results_postgres(results) click to toggle source
# File lib/mmtrix/agent/database.rb, line 170
def process_explain_results_postgres(results)
  if results.is_a?(String)
    query_plan_string = results
  else
    lines = []
    results.each { |row| lines << row[QUERY_PLAN] }
    query_plan_string = lines.join("\n")
  end

  unless record_sql_method == :raw
    query_plan_string = Mmtrix::Agent::Database::PostgresExplainObfuscator.obfuscate(query_plan_string)
  end
  values = query_plan_string.split("\n").map { |line| [line] }

  [[QUERY_PLAN], values]
end
process_explain_results_sqlite(results) click to toggle source
# File lib/mmtrix/agent/database.rb, line 228
def process_explain_results_sqlite(results)
  return string_explain_plan_results(results) if results.is_a?(String)
  headers = SQLITE_EXPLAIN_COLUMNS
  values  = []
  results.each do |row|
    values << headers.map { |h| row[h] }
  end
  [headers, values]
end
process_resultset(results, adapter) click to toggle source
# File lib/mmtrix/agent/database.rb, line 155
def process_resultset(results, adapter)
  case adapter.to_s
  when 'postgres', 'postgresql'
    process_explain_results_postgres(results)
  when 'mysql2'
    process_explain_results_mysql2(results)
  when 'mysql'
    process_explain_results_mysql(results)
  when 'sqlite'
    process_explain_results_sqlite(results)
  end
end
record_sql_method(config_section=:transaction_tracer) click to toggle source
# File lib/mmtrix/agent/database.rb, line 51
def record_sql_method(config_section=:transaction_tracer)
  key = record_sql_method_key(config_section)

  case Agent.config[key].to_s
  when 'off'
    :off
  when 'none'
    :off
  when 'false'
    :off
  when 'raw'
    :raw
  else
    :obfuscated
  end
end
record_sql_method_key(config_section) click to toggle source
# File lib/mmtrix/agent/database.rb, line 68
def record_sql_method_key(config_section)
  case config_section
  when :transaction_tracer
    :'transaction_tracer.record_sql'
  when :slow_sql
    :'slow_sql.record_sql'
  else
    "#{config_section}.record_sql".to_sym
  end
end
set_sql_obfuscator(type, &block) click to toggle source
# File lib/mmtrix/agent/database.rb, line 47
def set_sql_obfuscator(type, &block)
  Obfuscator.instance.set_sql_obfuscator(type, &block)
end
should_collect_explain_plans?(config_section=:transaction_tracer) click to toggle source
# File lib/mmtrix/agent/database.rb, line 85
def should_collect_explain_plans?(config_section=:transaction_tracer)
  should_record_sql?(config_section) &&
    Agent.config["#{config_section}.explain_enabled".to_sym]
end
should_record_sql?(config_section=:transaction_tracer) click to toggle source
# File lib/mmtrix/agent/database.rb, line 81
def should_record_sql?(config_section=:transaction_tracer)
  RECORD_FOR.include?(record_sql_method(config_section))
end
string_explain_plan_results(results) click to toggle source

Sequel returns explain plans as just one big pre-formatted String In that case, we send a nil headers array, and the single string wrapped in an array for the values. Note that we don’t use this method for Postgres explain plans, since they need to be passed through the explain plan obfuscator first.

# File lib/mmtrix/agent/database.rb, line 192
def string_explain_plan_results(results)
  [nil, [results]]
end
truncate_query(query) click to toggle source
# File lib/mmtrix/agent/database.rb, line 35
def truncate_query(query)
  if query.length > (MAX_QUERY_LENGTH - 4)
    query[0..MAX_QUERY_LENGTH - 4] + '...'
  else
    query
  end
end