Module: TingYun::Agent::Database

Extended by:
Database
Included in:
Database
Defined in:
lib/ting_yun/agent/database.rb

Defined Under Namespace

Classes: ConnectionManager, Obfuscator, Statement

Constant Summary collapse

MAX_QUERY_LENGTH =
16384
RECORD_FOR =
[:raw, :obfuscated].freeze
SUPPORTED_ADAPTERS_FOR_EXPLAIN =
%w[postgres postgresql mysql2 mysql sqlite].freeze
QUERY_PLAN =
'QUERY PLAN'.freeze
SQLITE_EXPLAIN_COLUMNS =
%w[addr opcode p1 p2 p3 p4 p5 comment]
KNOWN_OPERATIONS =
[
    'alter',
    'select',
    'update',
    'delete',
    'insert',
    'create',
    'show',
    'set',
    'exec',
    'execute',
    'call'
]
SQL_COMMENT_REGEX =
Regexp.new('/\*.*?\*/', Regexp::MULTILINE).freeze
EMPTY_STRING =
''.freeze

Instance Method Summary collapse

Instance Method Details

#adapter_from_config(config) ⇒ Object



89
90
91
92
93
94
95
96
97
# File 'lib/ting_yun/agent/database.rb', line 89

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



18
19
20
# File 'lib/ting_yun/agent/database.rb', line 18

def capture_query(query)
  TingYun::Helper.correctly_encoded(truncate_query(query))
end

#close_connectionsObject



226
227
228
# File 'lib/ting_yun/agent/database.rb', line 226

def close_connections
  ConnectionManager.instance.close_connections
end

#explain(sql, config, explainer = nil) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ting_yun/agent/database.rb', line 63

def explain(sql, config, explainer=nil)

  return unless explainer && is_select?(sql)

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

  if parameterized?(sql)
    TingYun::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)
    TingYun::Agent.logger.debug("Not collecting explain plan because an unknown connection adapter ('#{adapter}') was used.")
    return
  end

  handle_exception_in_explain do
    plan = explainer.call(config, sql)
    return process_resultset(plan, adapter) if plan
  end
end

#explain_sql(sql, config, explainer = nil) ⇒ Object



54
55
56
57
58
59
# File 'lib/ting_yun/agent/database.rb', line 54

def explain_sql(sql, config, explainer=nil)
  return nil unless sql && explainer && config
  _sql = sql.split(";\n")[0] # only explain the first
  explain_plan = explain(_sql, config, explainer)
  return explain_plan || {"dialect"=> nil, "keys"=>[], "values"=>[]}
end

#get_connection(config, &connector) ⇒ Object



222
223
224
# File 'lib/ting_yun/agent/database.rb', line 222

def get_connection(config, &connector)
  ConnectionManager.instance.get_connection(config, &connector)
end

#handle_exception_in_explainObject



214
215
216
217
218
219
# File 'lib/ting_yun/agent/database.rb', line 214

def handle_exception_in_explain
  yield
rescue => e
  ::TingYun::Agent.logger.error("Error getting query plan:", e)
  nil
end

#is_select?(sql) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/ting_yun/agent/database.rb', line 104

def is_select?(sql)
  parse_operation_from_query(sql) == 'select'
end

#obfuscate_sql(sql) ⇒ Object



13
14
15
# File 'lib/ting_yun/agent/database.rb', line 13

def obfuscate_sql(sql)
  Obfuscator.instance.obfuscator.call(sql)
end

#parameterized?(sql) ⇒ Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/ting_yun/agent/database.rb', line 100

def parameterized?(sql)
  Obfuscator.instance.obfuscate_single_quote_literals(sql) =~ /\$\d+/
end

#parse_operation_from_query(sql) ⇒ Object



205
206
207
208
209
210
211
# File 'lib/ting_yun/agent/database.rb', line 205

def parse_operation_from_query(sql)
  sql = TingYun::Helper.correctly_encoded(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) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ting_yun/agent/database.rb', line 152

def process_explain_results_mysql(results)
  return string_explain_plan_results("MySQL", 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
  {"dialect"=> "MySQL", "keys"=>headers, "values"=>values}
end

#process_explain_results_mysql2(results) ⇒ Object



144
145
146
147
148
149
150
# File 'lib/ting_yun/agent/database.rb', line 144

def process_explain_results_mysql2(results)
  return string_explain_plan_results("MySQL", results) if results.is_a?(String)
  headers = results.fields
  values  = []
  results.each { |row| values << row }
  {"dialect"=> "MySQL", "keys"=>headers, "values"=>values}
end

#process_explain_results_postgres(results) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ting_yun/agent/database.rb', line 123

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("nbs.action_tracer.record_sql") == :raw
    query_plan_string = Obfuscator.instance.obfuscate_postgres_explain(query_plan_string)
  end
  values = query_plan_string.split("\n").map { |line| [line] }

  {"dialect"=> "PostgreSQL", "keys"=>[QUERY_PLAN], "values"=>values}
end

#process_explain_results_sqlite(results) ⇒ Object



176
177
178
179
180
181
182
183
184
# File 'lib/ting_yun/agent/database.rb', line 176

def process_explain_results_sqlite(results)
  return string_explain_plan_results("sqlite", results) if results.is_a?(String)
  headers = SQLITE_EXPLAIN_COLUMNS
  values  = []
  results.each do |row|
    values << headers.map { |h| row[h] }
  end
  {"dialect"=> "sqlite", "keys"=>headers, "values"=>values}
end

#process_resultset(results, adapter) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ting_yun/agent/database.rb', line 108

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



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/ting_yun/agent/database.rb', line 37

def record_sql_method(key)

  case Agent.config[key].to_s
    when 'off'
      :off
    when 'raw'
      :raw
    else
      :obfuscated
  end
end

#should_action_collect_explain_plans?Boolean

Returns:

  • (Boolean)


49
50
51
52
# File 'lib/ting_yun/agent/database.rb', line 49

def should_action_collect_explain_plans?
  should_record_sql?("nbs.action_tracer.record_sql") &&
      Agent.config["nbs.action_tracer.explain_enabled".to_sym]
end

#should_record_sql?(key) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/ting_yun/agent/database.rb', line 33

def should_record_sql?(key)
  RECORD_FOR.include?(record_sql_method(key.to_sym))
end

#string_explain_plan_results(adpater, results) ⇒ Object



140
141
142
# File 'lib/ting_yun/agent/database.rb', line 140

def string_explain_plan_results(adpater, results)
  {"dialect"=> adpater, "keys"=>[], "values"=>[results]}
end

#truncate_query(query) ⇒ Object



22
23
24
25
26
27
28
# File 'lib/ting_yun/agent/database.rb', line 22

def truncate_query(query)
  if query.length > (MAX_QUERY_LENGTH - 4)
    query[0..MAX_QUERY_LENGTH - 4] + '...'
  else
    query
  end
end