newrelic
Version:
New Relic agent
220 lines (182 loc) • 5.86 kB
JavaScript
'use strict'
var a = require('async')
var logger = require('../logger').child({component: 'query_tracer'})
var obfuscate = require('../util/sql/obfuscate')
var Stats = require('../stats')
var util = require('util')
var crypto = require('crypto')
var encode = require('../util/codec').encode
var path = require('path')
var NR_ROOT = path.resolve(__dirname, '..')
module.exports = QueryTracer
function QueryTracer(config) {
if (!(this instanceof QueryTracer)) {
return new QueryTracer(config)
}
this.samples = new Map()
this.config = config
}
QueryTracer.prototype.removeShortest = function removeShortest() {
let shortest = null
for (let sample of this.samples.values()) {
const trace = sample.trace
if (!shortest || shortest.duration > trace.duration) {
shortest = trace
}
}
this.samples.delete(shortest.normalized)
}
QueryTracer.prototype.merge = function merge(tracer) {
for (let sample of tracer.samples.values()) {
let ownSample = this.samples.get(sample.trace.normalized)
if (ownSample) {
ownSample.merge(sample)
} else {
this.samples.set(sample.trace.normalized, sample)
}
}
}
QueryTracer.prototype.addQuery = function addQuery(segment, type, query, trace) {
const ttConfig = this.config.transaction_tracer
// If DT is enabled and the segment is part of a sampled transaction
// (i.e. we are creating a span event for this segment), then we need
// to collect the sql trace.
var slowQuery
switch (ttConfig.record_sql) {
case 'raw':
slowQuery = new SlowQuery(segment, type, query, trace)
logger.trace('recording raw sql')
segment.parameters.sql = slowQuery.query
break
case 'obfuscated':
slowQuery = new SlowQuery(segment, type, query, trace)
logger.trace('recording obfuscated sql')
segment.parameters.sql_obfuscated = slowQuery.obfuscated
break
default:
logger.trace(
'not recording sql statement, transaction_tracer.record_sql was set to %s',
ttConfig.record_sql
)
return
}
if (segment.getDurationInMillis() < ttConfig.explain_threshold) {
return
}
slowQuery = slowQuery || new SlowQuery(segment, type, query, trace)
segment.parameters.backtrace = slowQuery.trace
if (!this.config.slow_sql.enabled) {
return
}
const ownSample = this.samples.get(slowQuery.normalized)
if (ownSample) {
return ownSample.aggregate(slowQuery)
}
this.samples.set(slowQuery.normalized, new QuerySample(this, slowQuery))
// Do not remove the shortest sample when in serverless mode, since
// sampling is disabled.
if (this.config.serverless_mode.enabled) {
return
}
if (this.samples.size > this.config.slow_sql.max_samples) {
this.removeShortest()
}
}
QueryTracer.prototype.prepareJSON = function prepareJSON(done) {
a.map(this.samples.values(), (sample, cb) => sample.prepareJSON(cb), done)
}
function QuerySample(tracer, slowQuery) {
Stats.call(this)
this.tracer = tracer
this.trace = slowQuery
this.aggregate(slowQuery)
}
util.inherits(QuerySample, Stats)
QuerySample.prototype.aggregate = function aggregate(slowQuery) {
this.recordValue(slowQuery.duration)
if (this.trace && this.trace.duration >= slowQuery.duration) return
this.trace = slowQuery
}
QuerySample.prototype.merge = function merge(sample) {
Stats.prototype.merge.call(this, sample)
if (this.trace.duration < sample.trace.duration) {
this.trace = sample.trace
}
}
QuerySample.prototype.prepareJSON = function prepareJSON(done) {
var transaction = this.trace.segment.transaction
var sample = this
var trace = sample.trace
var params = sample.getParams()
if (!this.tracer.config.simple_compression) {
encode(params, respond)
} else {
process.nextTick(respond.bind(null, null, params))
}
function respond(err, data) {
if (err) return done(err)
done(null, [
transaction.getFullName(),
transaction.url || '<unknown>',
trace.id,
getQuery(sample.tracer.config, trace),
trace.metric,
sample.callCount,
sample.total,
sample.min,
sample.max,
data
])
}
}
QuerySample.prototype.getParams = function getParams() {
var segmentParams = this.trace.segment.parameters
var params = {
backtrace: this.trace.trace,
}
if (segmentParams.host) {
params.host = segmentParams.host
}
if (segmentParams.port_path_or_id) {
params.port_path_or_id = segmentParams.port_path_or_id
}
if (segmentParams.database_name) {
params.database_name = segmentParams.database_name
}
if (this.tracer.config.distributed_tracing.enabled) {
this.trace.segment.transaction.addDistributedTraceIntrinsics(params)
}
return params
}
function SlowQuery(segment, type, query, trace) {
this.obfuscated = obfuscate(query, type)
this.normalized = this.obfuscated.replace(/\?\s*,\s*|\s*/g, '')
this.id = normalizedHash(this.normalized)
this.segment = segment
this.query = query
this.metric = segment.name
this.trace = formatTrace(trace)
this.duration = segment.getDurationInMillis()
}
function normalizedHash(value) {
// We leverage the last 16 hex digits of which would mostly fit in a long and
// rely on parseInt to drop bits that do not fit in a JS number
return parseInt(crypto.createHash('sha1').update(value).digest('hex').slice(-16), 16)
}
function formatTrace(trace) {
// remove error message and instrumentation frames from stack trace
return trace ? trace.split('\n').slice(1).filter(notNR).join('\n') : ''
}
function notNR(frame) {
return frame.indexOf(NR_ROOT) === -1
}
function getQuery(config, trace) {
switch (config.transaction_tracer.record_sql) {
case 'raw':
return trace.query
case 'obfuscated':
return trace.obfuscated
default:
return '?'
}
}