newrelic
Version:
New Relic agent
323 lines (287 loc) • 11 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const Stats = require('../stats')
const ApdexStats = require('../stats/apdex.js')
const NAMES = require('./names')
/*
*
* CONSTANTS
*
*/
const FROM_MILLIS = 1e-3
/**
* A metric is a set of aggregated data (summary statistics) associated with a
* metric name. Some metrics belong to scopes, which are typically the name of
* a transaction or a background task. This class is a collection of mappings
* from names (or scopes and names) to data, as well as functions for
* manipulating those data directly. It also can produce a serialized
* representation suitable for stringifying into JSON and sending to the
* collector.
*
* There are several metrics collections in existence at any given time. Each
* agent has one metrics collection, which is created at the beginning of each
* harvest cycle. Each new transaction also gets its own metrics collection,
* which is merged into the agent's metrics when the transaction is finalized.
* This allows each set of metrics to be added to the harvest cycle atomically,
* which guarantees that each transaction will not have its metrics split
* across multiple harvest cycles. If delivery to the collector fails, the
* metrics collection associated with the failed delivery can be merged back
* into the metrics collection for the ongoing harvest cycle.
*
* Metrics can be remapped, which is a process by which they are assigned a
* short, numerical ID by New Relic. This can shrink the serialized JSON
* considerably. The mapping from transaction name (and scope) happens only
* at serialization time, which allows the mappings from name to ID to happen
* on the fly.
*
* @param {number} apdexT The apdex-tolerating value, for use in creating apdex
* statistics.
* @param {MetricMapper} mapper The mapper that turns metric names into IDs.
* @param {NameNormalizer} normalizer The normalizer that standardizes metric names.
*/
function Metrics(apdexT, mapper, normalizer) {
if (apdexT == null || apdexT === '') {
throw new Error('metrics must be created with apdexT')
}
if (!mapper) {
throw new Error('metrics must be created with a mapper')
}
if (!normalizer) {
throw new Error('metrics must be created with a name normalizer')
}
this.empty = true
this.started = Date.now()
this.apdexT = apdexT
this.mapper = mapper
this.normalizer = normalizer
this.unscoped = Object.create(null) // {name : stats}
this.scoped = Object.create(null) // {scope : {name : stats}}
}
/**
* This is the preferred way for interacting with metrics. Set the duration
* (and optionally the amount of that duration that was exclusive to that
* particular metric and not any child operations to that metric) of an
* operation. If there are no data for the name (and optional scope) existing,
* the collection will create a set of data before recording the measurement.
*
* @param {string} name The name of the metric.
* @param {string} scope (Optional) The scope to which the metric belongs.
* @param {number} duration The duration of the related operation, in milliseconds.
* @param {number} exclusive (Optional) The portion of the operation specific to this
* metric.
* @returns {Stats} The aggregated data related to this metric.
*/
Metrics.prototype.measureMilliseconds = measureMilliseconds
function measureMilliseconds(name, scope, duration, exclusive) {
const stats = this.getOrCreateMetric(name, scope)
stats.recordValueInMillis(duration, exclusive)
return stats
}
/**
* Set the size of an operation. If there are no data for the name existing,
* the collection will create a set of data before recording the measurement.
*
* @param {string} name The name of the metric.
* @param {number} size The size of the related operation, in bytes.
* @param {number} exclusiveSize The exclusive size of the related operation, in megabytes.
* @param {boolean} exact If true, size is interpreted as bytes rather than megabytes
* @returns {Stats} The aggregated data related to this metric.
*/
Metrics.prototype.measureBytes = function measureBytes(name, size, exclusiveSize, exact) {
const stats = this.getOrCreateMetric(name)
stats.recordValueInBytes(size, exclusiveSize, exact)
return stats
}
/**
* Look up the mapping from a name (and optionally a scope) to a set of metric
* data for that name, creating the data if they don't already exist.
*
* @param {string} name The name of the requested metric.
* @param {string} scope (Optional) The scope to which the metric is bound.
* @returns {Stats} The aggregated data for that name.
*/
Metrics.prototype.getOrCreateMetric = function getOrCreateMetric(name, scope) {
const resolved = this._resolve(scope)
let stats = resolved[name]
if (!stats) {
this.empty = false
stats = resolved[name] = new Stats()
}
return stats
}
/**
* Look up the mapping from a name (and optionally a scope) to a set of metric
* apdex data for that name, creating the data if they don't already exist.
*
* @param {string} name The name of the requested metric.
* @param {string} scope The scope to which the metric is bound
* (optional).
* @param {number} overrideApdex A custom apdexT for this metric, in
* milliseconds. This will be the same for
* a given run, because key transaction metrics
* are set at connect time via server-side
* configuration.
* @returns {ApdexStats} The aggregated data for that name.
*/
Metrics.prototype.getOrCreateApdexMetric = getOrCreateApdexMetric
function getOrCreateApdexMetric(name, scope, overrideApdex) {
if (!name) {
throw new Error('Metrics must be named')
}
const resolved = this._resolve(scope)
if (!resolved[name]) {
this.empty = false
// Only use the given override to create the metric if this is not the
// global apdex AND we have a valid value.
const apdexT =
name !== NAMES.APDEX && overrideApdex > 0 ? overrideApdex * FROM_MILLIS : this.apdexT
resolved[name] = new ApdexStats(apdexT)
}
return resolved[name]
}
/**
* Look up a metric, and don't create it if it doesn't exist. Can create scopes
* as a byproduct, but this function is only intended for use in testing, so
* it's not a big deal.
*
* @param {string} name Metric name.
* @param {string} scope (Optional) The scope, if any, to which the metric
* belongs.
* @returns {object} Either a stats aggregate, an apdex stats aggregate, or
* undefined.
*/
Metrics.prototype.getMetric = function getMetric(name, scope) {
if (!name) {
throw new Error('Metrics must be named')
}
return this._resolve(scope)[name]
}
/**
* Convert this collection into a representation suitable for serialization
* by JSON.stringify and delivery to the collector. Hope you like nested
* arrays!
*
* @returns {object} Set of nested arrays containing metric information.
*/
Metrics.prototype.toJSON = function toJSON() {
return this._toUnscopedData().concat(this._toScopedData())
}
/**
* Combine two sets of metric data. Intended to be used as described above,
* either when folding a transaction's metrics into the agent's metrics for
* later harvest, or one harvest cycle's metrics into the next when a
* delivery attempt to the collector fails. Among the more performance-
* critical pieces of code in the agent, so some performance tuning would
* probably be a good idea.
*
* @param {Metrics} other
* The collection to be folded into this one.
* @param {boolean} adjustStartTime
* If the start time for the timeslice should be adjusted.
*/
Metrics.prototype.merge = function merge(other, adjustStartTime) {
this.empty = this.empty && other.empty
if (adjustStartTime) {
this.started = Math.min(this.started, other.started)
}
_merge(this.unscoped, other.unscoped)
// Loop through all scopes and merge them. Since we know `.scoped` has a `null`
// prototype we don't need to worry about own property checks.
for (const scope in other.scoped) {
_merge(this._resolve(scope), other.scoped[scope])
}
}
function _merge(a, b) {
for (const name in b) {
if (a[name]) {
a[name].merge(b[name])
} else {
a[name] = b[name]
}
}
}
/**
* Look up the metric namespace belonging to a scope, creating it if it doesn't
* already exist.
*
* @param {string} scope (Optional) The scope to look up.
* @returns {object} The namespace associated with the provided scope, or the
* un-scoped metrics if the scope isn't set.
*/
Metrics.prototype._resolve = function _resolve(scope) {
let resolved = this.unscoped
if (scope) {
resolved = this.scoped[scope]
if (!resolved) {
resolved = this.scoped[scope] = Object.create(null)
}
}
return resolved
}
/**
* Map a metric to its nested-array representation, applying any name -> ID
* mappings along the way. Split from _getScopedData for performance.
*
* @param {string} name The string to look up.
*/
Metrics.prototype._getUnscopedData = function _getUnscopedData(name) {
if (!this.unscoped[name]) {
return
}
const normalized = this.normalizer.normalize(name)
if (normalized.ignore || !normalized.value) {
return
}
return [this.mapper.map(normalized.value), this.unscoped[name]]
}
/**
* Map a metric to its nested-array representation, applying any name -> ID
* mappings along the way. Split from _getUnscopedData for performance.
*
* @param {string} name The string to look up.
* @param {string} scope The scope to map with.
*/
Metrics.prototype._getScopedData = function _getScopedData(name, scope) {
if (!this.scoped[scope][name]) {
return
}
const normalized = this.normalizer.normalize(name)
if (normalized.ignore || !normalized.value) {
return
}
return [this.mapper.map(normalized.value, scope), this.scoped[scope][name]]
}
/**
* @returns {object} A serializable version of the unscoped metrics. Intended
* for use by toJSON.
*/
Metrics.prototype._toUnscopedData = function _toUnscopedData() {
const metricData = []
for (const name of Object.keys(this.unscoped)) {
const data = this._getUnscopedData(name)
if (data) {
metricData.push(data)
}
}
return metricData
}
/**
* @returns {object} A serializable version of the scoped metrics. Intended for
* use by toJSON.
*/
Metrics.prototype._toScopedData = function _toScopedData() {
const metricData = []
for (const [key, scope] of Object.entries(this.scoped)) {
for (const name of Object.keys(scope)) {
const data = this._getScopedData(name, key)
if (data) {
metricData.push(data)
}
}
}
return metricData
}
module.exports = Metrics