newrelic
Version:
New Relic agent
276 lines (241 loc) • 6.71 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const logger = require('../logger.js').child({ component: 'name-state' })
const NAMES = require('../metrics/names')
// TODO: Figure out a way to phase out legacy transaction names.
const LEGACY_NAMING = {
Custom: true,
Connect: true,
Director: true,
Expressjs: true,
Hapi: true,
Nodejs: true,
Restify: true
}
const STATUS_CODE_NAMES = {
404: '(not found)',
501: '(not implemented)',
405: '(method not allowed)'
}
/**
* Manages transaction names using a stack of paths.
*
* @param prefix
* @param verb
* @param delimiter
* @param path
* @class
*/
function NameState(prefix, verb, delimiter, path) {
this.reset()
this.setName(prefix, verb, delimiter, path)
this._frozen = false
}
NameState.prototype.setName = function setName(prefix, verb, delimiter, path) {
if (this._frozen) {
return
}
this.setPrefix(prefix)
this.verb = verb && verb.toUpperCase()
this.delimiter = delimiter
this.pathStack = path ? [{ path, params: null }] : []
this._pathCache = null
this.markedPath = []
logger.trace('setName called on name state, path stack now %j', this.pathStack)
}
NameState.prototype.getStatusName = function getStatusName(statusCode) {
const name = STATUS_CODE_NAMES[statusCode]
if (name) {
if (Object.prototype.hasOwnProperty.call(LEGACY_NAMING, this.prefix)) {
return _getName(this, name)
}
return NAMES.WEB.FRAMEWORK_PREFIX + '/' + _getName(this, name)
}
}
NameState.prototype.markPath = function markPath() {
this.markedPath = this.pathStack.slice()
}
/**
* Sets the metric prefix (i.e. Expressjs).
*
* @param prefix
*/
NameState.prototype.setPrefix = function setPrefix(prefix) {
if (this._frozen) {
return
}
if (prefix === null) {
this.prefix = null
return
}
this.prefix = prefix[prefix.length - 1] === '/' ? prefix.substring(0, prefix.length - 1) : prefix
}
/**
* Sets the HTTP verb (i.e. GET/POST/PUT)
*
* @param verb
*/
NameState.prototype.setVerb = function setVerb(verb) {
if (!this._frozen) {
this.verb = verb && verb.toUpperCase()
}
}
/**
* Sets the delimiter character used to separate the http verb from the path.
*
* @param delimiter
*/
NameState.prototype.setDelimiter = function setDelimiter(delimiter) {
if (!this._frozen) {
this.delimiter = delimiter
}
}
NameState.prototype.isEmpty = function isEmpty() {
return this.pathStack.length === 0 && this.markedPath.length === 0
}
/**
* Pushes a new path element onto the naming stack.
*
* @param path
* @param params
*/
NameState.prototype.appendPath = function appendPath(path, params) {
if (!this._frozen && path != null) {
const strPath = path instanceof RegExp ? path.source : String(path)
this.pathStack.push({ path: strPath, params: params || null })
if (path !== '') {
this._pathCache = null
}
logger.trace('Appended %s to path stack', strPath)
}
}
/**
* Pushes a new path element onto the naming stack if the stack is
* empty.
*
* @param path
* @param params
*/
NameState.prototype.appendPathIfEmpty = function appendPathIfEmpty(path, params) {
if (!this._frozen && this.isEmpty()) {
return this.appendPath(path, params || null)
}
}
/**
* Pops the last element off the name stack.
*
* If `path` is provided, the stack is popped back to the first element matching
* `path`. If no element matches, the stack is left unchanged.
*
* @param {string} [path] - Optional. A path piece to pop back to.
*/
NameState.prototype.popPath = function popPath(path) {
if (this._frozen || this.pathStack.length === 0) {
return
}
this._pathCache = null
let pops = 0
if (path != null) {
const idx = _findLastIndex(this.pathStack, (a) => a.path === path)
if (idx !== -1) {
pops = this.pathStack.length - idx
this.pathStack.splice(idx)
}
} else {
pops = 1
this.pathStack.pop()
}
logger.trace('Popped %j from path, %d removed', path, pops)
}
NameState.prototype.getPath = function getPath() {
const ps = !this.pathStack.length ? this.markedPath : this.pathStack
const psLength = ps.length
if (this._pathCache) {
return this._pathCache
} else if (psLength === 0) {
return null // nameState initialized but never set
}
let path = '/'
for (let i = 0; i < psLength; ++i) {
let a = ps[i].path
if (a && a !== '/') {
if (a[0] !== '/' && path[path.length - 1] !== '/') {
path += '/'
} else if (a[0] === '/' && path[path.length - 1] === '/') {
a = a.substring(1)
}
path += a
}
}
this._pathCache = path
return this._pathCache
}
NameState.prototype.getNameNotFound = function getNameNotFound() {
const name = _getName(this, '(not found)')
if (Object.prototype.hasOwnProperty.call(LEGACY_NAMING, this.prefix)) {
return name
}
return NAMES.WEB.FRAMEWORK_PREFIX + '/' + name
}
NameState.prototype.getName = function getName() {
const path = this.getPath()
if (path === null) {
return null
}
return _getName(this, path)
}
NameState.prototype.getFullName = function getFullName() {
const name = this.getName()
if (Object.prototype.hasOwnProperty.call(LEGACY_NAMING, this.prefix)) {
return name
}
return NAMES.WEB.FRAMEWORK_PREFIX + '/' + name
}
NameState.prototype.forEachParams = function forEachParams(fn, ctx) {
this.pathStack.forEach(function forEachPathStack(a) {
if (a.params) {
fn.call(ctx, a.params)
}
})
}
/**
* Locks the name state, preventing future changes from taking effect.
*/
NameState.prototype.freeze = function freeze() {
this._frozen = true
}
NameState.prototype.reset = function reset() {
if (this._frozen) {
return
}
logger.trace('Reset called on name state, path stack was %j', this.pathStack)
this.prefix = null
this.verb = null
this.delimiter = null
this.pathStack = []
this._pathCache = null
}
function _getName(nameState, path) {
const verb = nameState.verb ? '/' + nameState.verb : ''
return (nameState.prefix || '') + verb + (nameState.delimiter || '') + path
}
/**
* Finds the last index of a single element in an array matching `pred`.
*
* @param {Array} arr - Array to search.
* @param {Function} pred - Predicate function that returns `true` on matches.
* @param {*} ctx - The `this` arg for `pred`.
* @returns {number} - This index of the last matching item, or `-1`.
*/
function _findLastIndex(arr, pred, ctx) {
for (let i = arr.length - 1; i >= 0; --i) {
if (pred.call(ctx, arr[i], i, arr)) {
return i
}
}
return -1
}
module.exports = NameState