dd-trace
Version:
Datadog APM tracing client for JavaScript
126 lines (102 loc) • 3.88 kB
JavaScript
'use strict'
const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
const CachePlugin = require('../../dd-trace/src/plugins/cache')
const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter')
const MAX_ARG_LENGTH = 100
const MAX_COMMAND_LENGTH = 1000
class RedisPlugin extends CachePlugin {
static id = 'redis'
static system = 'redis'
/** @type {string} */
#rawCommandKey
/** @type {Map<string | undefined, { name: string, source: string | undefined }>} */
#serviceByConnection = new Map()
// `nomenclature.config` identity at last cache fill. `withNamingSchema` swaps it without
// running this plugin's `configure`, so identity drives invalidation in `bindStart`.
/** @type {object | undefined} */
#lastNomenclatureConfig
/** @type {string | undefined} */
#cachedOperationName
constructor (...args) {
super(...args)
this._spanType = 'redis'
}
bindStart (ctx) {
const { db, command, args, argsStartIndex, connectionOptions, connectionName } = ctx
const resource = command
const normalizedCommand = command.toUpperCase()
if (!this.config.filter(normalizedCommand)) {
return { noop: true }
}
const nomConfig = this._tracer._nomenclature.config
if (this.#lastNomenclatureConfig !== nomConfig) {
this.#lastNomenclatureConfig = nomConfig
this.#cachedOperationName = undefined
this.#serviceByConnection.clear()
}
let service = this.#serviceByConnection.get(connectionName)
if (service === undefined) {
service = this.serviceName({ pluginConfig: this.config, system: this.system, connectionName })
this.#serviceByConnection.set(connectionName, service)
}
this.startSpan({
resource,
service,
type: this._spanType,
meta: {
'db.type': this._spanType,
'db.name': db || '0',
[this.#rawCommandKey]: formatCommand(normalizedCommand, args, argsStartIndex),
'out.host': connectionOptions.host,
[CLIENT_PORT_KEY]: connectionOptions.port,
},
}, ctx)
return ctx.currentStore
}
operationName () {
this.#cachedOperationName ??= super.operationName()
return this.#cachedOperationName
}
configure (config) {
super.configure(normalizeConfig(config))
// Subclasses (iovalkey) overwrite `_spanType` in their constructor, before any `configure`,
// so reading it here is stable.
this.#rawCommandKey = `${this._spanType}.raw_command`
this.#lastNomenclatureConfig = undefined
this.#cachedOperationName = undefined
this.#serviceByConnection.clear()
}
}
function formatCommand (command, args, argsStartIndex = 0) {
if (!args || command === 'AUTH') return command
let result = command
for (let i = argsStartIndex, l = args.length; i < l; i++) {
const arg = args[i]
if (typeof arg === 'function') continue
result = `${result} ${formatArg(arg)}`
if (result.length > MAX_COMMAND_LENGTH) return result.slice(0, MAX_COMMAND_LENGTH - 3) + '...'
}
return result
}
function formatArg (arg) {
if (typeof arg === 'string') {
return arg.length > MAX_ARG_LENGTH ? arg.slice(0, MAX_ARG_LENGTH - 3) + '...' : arg
}
// Number stringification is bounded (~23 chars max), so it never hits MAX_ARG_LENGTH.
if (typeof arg === 'number') return String(arg)
return '?'
}
function normalizeConfig (config) {
if (config.allowlist) uppercaseAllEntries(config.allowlist)
if (config.whitelist) uppercaseAllEntries(config.whitelist)
if (config.blocklist) uppercaseAllEntries(config.blocklist)
if (config.blacklist) uppercaseAllEntries(config.blacklist)
const filter = urlFilter.getFilter(config)
return { ...config, filter }
}
function uppercaseAllEntries (entries) {
for (let i = 0; i < entries.length; i++) {
entries[i] = String(entries[i]).toUpperCase()
}
}
module.exports = RedisPlugin