UNPKG

newrelic

Version:
429 lines (380 loc) 14 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const NO_MATCH = -Infinity const EXACT_MATCH = Infinity const DESTINATIONS = { NONE: 0x00, TRANS_EVENT: 0x01, TRANS_TRACE: 0x02, ERROR_EVENT: 0x04, BROWSER_EVENT: 0x08, SPAN_EVENT: 0x10, TRANS_SEGMENT: 0x20 } DESTINATIONS.TRANS_SCOPE = DESTINATIONS.TRANS_EVENT | DESTINATIONS.TRANS_TRACE | DESTINATIONS.ERROR_EVENT | DESTINATIONS.BROWSER_EVENT DESTINATIONS.SEGMENT_SCOPE = DESTINATIONS.SPAN_EVENT | DESTINATIONS.TRANS_SEGMENT DESTINATIONS.TRANS_COMMON = DESTINATIONS.TRANS_EVENT | DESTINATIONS.TRANS_TRACE | DESTINATIONS.ERROR_EVENT DESTINATIONS.LIMITED = DESTINATIONS.TRANS_TRACE | DESTINATIONS.ERROR_EVENT const TRANS_SCOPE_DETAILS = [ { id: DESTINATIONS.TRANS_EVENT, key: 'TRANS_EVENT', name: 'transaction_events' }, { id: DESTINATIONS.TRANS_TRACE, key: 'TRANS_TRACE', name: 'transaction_tracer' }, { id: DESTINATIONS.ERROR_EVENT, key: 'ERROR_EVENT', name: 'error_collector' }, { id: DESTINATIONS.BROWSER_EVENT, key: 'BROWSER_EVENT', name: 'browser_monitoring' } ] const SEGMENT_SCOPE_DETAILS = [ { id: DESTINATIONS.SPAN_EVENT, key: 'SPAN_EVENT', name: 'span_events' }, { id: DESTINATIONS.TRANS_SEGMENT, key: 'TRANS_SEGMENT', name: 'transaction_segments' } ] const DESTINATION_DETAILS = [...TRANS_SCOPE_DETAILS, ...SEGMENT_SCOPE_DETAILS] module.exports = exports = AttributeFilter exports.DESTINATIONS = DESTINATIONS /** * Parses configuration for filtering attributes and provides way to test keys * against the configuration. * * @class * @private * @param {object} config - The configuration object for the agent. */ function AttributeFilter(config) { this.config = config this._rules = Object.create(null) this._cache = Object.create(null) this._cachedCount = 0 this._enabledDestinations = DESTINATIONS.NONE const updater = this.update.bind(this) // Add the global rules. config.on('attributes.enabled', updater) config.on('attributes.include', updater) config.on('attributes.exclude', updater) this._rules.global = Object.create(null) // And all the destination rules. DESTINATION_DETAILS.forEach(function forEachDestination(dest) { config.on(dest.name + '.attributes.enabled', updater) config.on(dest.name + '.attributes.include', updater) config.on(dest.name + '.attributes.exclude', updater) this._rules[dest.name] = Object.create(null) }, this) // Now pull in all the rules. this.update() } /** * Tests a given key against the global and destination transaction filters. * * @param {DESTINATIONS} destinations - The locations the attribute wants to be put. * @param {string} key - The name of the attribute to test. * @returns {DESTINATIONS} The destinations the attribute should be put. */ AttributeFilter.prototype.filterTransaction = filterTransaction function filterTransaction(destinations, key) { return this._filter(TRANS_SCOPE_DETAILS, destinations, key) } /** * Tests a given key against the global and destination segment filters. * * @param {DESTINATIONS} destinations - The locations the attribute wants to be put. * @param {string} key - The name of the attribute to test. * @returns {DESTINATIONS} The destinations the attribute should be put. */ AttributeFilter.prototype.filterSegment = function filterSegment(destinations, key) { return this._filter(SEGMENT_SCOPE_DETAILS, destinations, key) } /** * Tests a given key against all global and destination filters. * * @param {DESTINATIONS} destinations - The locations the attribute wants to be put. * @param {string} key - The name of the attribute to test. * @returns {DESTINATIONS} The destinations the attribute should be put. */ AttributeFilter.prototype.filterAll = function filterSegment(destinations, key) { return this._filter(DESTINATION_DETAILS, destinations, key) } /** * Tests a given key against the global and destination filters. * * @param {Array} scope - The destination details for filtering. * @param {DESTINATIONS} destinations - The locations the attribute wants to be put. * @param {string} key - The name of the attribute to test. * @returns {DESTINATIONS} The destinations the attribute should be put. */ AttributeFilter.prototype._filter = function _filter(scope, destinations, key) { // This method could be easily memoized since for a given destination and key // the result will always be the same until a configuration update happens. A // given application will also have a controllable set of destinations and // keys to check. // First, see if attributes are even enabled for this destination. if (!this.config.attributes.enabled) { return DESTINATIONS.NONE } // These are lazy computed to avoid calculating them for cached results. let globalInclude = null let globalExclude = null // Iterate over each destination and see if the rules apply. for (let i = 0; i < scope.length; ++i) { const dest = scope[i] const destId = dest.id const destName = dest.name if (!(this._enabledDestinations & destId)) { destinations &= ~destId // Remove this destination. continue } // Check for a cached result for this key. let result = this._cache[destName][key] if (result === undefined) { if (globalInclude === null) { globalInclude = _matchRules(this._rules.global.include, key) globalExclude = _matchRules(this._rules.global.exclude, key) } // Freshly calculate this attribute. result = _doTest(globalInclude, globalExclude, this._rules[destName], key) if (this._cachedCount < this.config.attributes.filter_cache_limit) { this._cache[destName][key] = result ++this._cachedCount } } destinations = _updateDestinations(destinations, destId, result) } return destinations } /** * Updates all the rules the given filter has access to. */ AttributeFilter.prototype.update = function update() { // Update the global rules. this._rules.global.include = _importRules( this.config.attributes.include_enabled ? this.config.attributes.include : [] ) this._rules.global.exclude = _importRules(this.config.attributes.exclude) this._cache = Object.create(null) this._cachedCount = 0 // And all the destination rules. DESTINATION_DETAILS.forEach(function forEachDestination(dest) { const name = dest.name if (!this.config[name].attributes.enabled) { return } this._enabledDestinations |= dest.id this._rules[name].include = _importRules( this.config.attributes.include_enabled ? this.config[name].attributes.include : [] ) this._rules[name].exclude = _importRules(this.config[name].attributes.exclude) this._cache[name] = Object.create(null) }, this) } /** * Applies the global and destination rules to this key. * * @private * @param {number} globalInclude Global inclusion match * @param {number} globalExclude Global exclusion match * @param {object} destConfig Destination specific include/exclude rules * @param {string} key The attribute name * @returns {boolean|number} True if this key is explicitly included, false if it is * explicitly excluded, or `NO_MATCH` if no rule applies. */ function _doTest(globalInclude, globalExclude, destConfig, key) { // Check for exclusion of the attribute. if (globalExclude === EXACT_MATCH) { return false } const destExclude = _matchRules(destConfig.exclude, key) if (destExclude === EXACT_MATCH) { return false } // Then check for inclusion of the attribute. if (globalInclude === EXACT_MATCH) { return true } const destInclude = _matchRules(destConfig.include, key) if (destInclude === EXACT_MATCH) { return true } // Did any rule match this key? If not, this is a no-match. if ( globalExclude === NO_MATCH && globalInclude === NO_MATCH && destExclude === NO_MATCH && destInclude === NO_MATCH ) { return NO_MATCH } // Something has matched this key, so compare the strength of any wildcard // matches that have happened. return ( // If destination include is a better match than either exclude, it's in! (destInclude > destExclude && destInclude >= globalExclude) || // If global include is a better match than either exclude, it's in! (globalInclude > destExclude && globalInclude > globalExclude) ) } /** * Tests the given key against the given rule set. * * This method assumes that the rule set is sorted from best possible match to * least possible match. Unsorted lists may result in a lesser score being given * to the value. * * @private * @param {Array.<string>} rules - The set of rules to match against. * @param {string} key - The name of the attribute to look for. * @returns {number} The strength of the match, from `0` for no-match to `Infinity` * for exact matches. */ function _matchRules(rules, key) { if (rules.exact && rules.exact.test(key)) { return EXACT_MATCH } const wildcard = rules.wildcard if (!wildcard) { return NO_MATCH } wildcard.lastIndex = 0 return wildcard.test(key) ? wildcard.lastIndex + 1 : NO_MATCH } /** * Converts the raw rules into a set of regular expressions to test against. * * @private * @param {Array.<string>} rules - The set of rules to compose. * @returns {object} An object with `exact` and `wildcard` properties which are * `RegExp` instances for testing keys. */ function _importRules(rules) { const out = { exact: null, wildcard: null } const exactRules = [] const wildcardRules = [] rules.forEach(function separateRules(rule) { if (rule[rule.length - 1] === '*') { wildcardRules.push(rule) } else { exactRules.push(rule) } }) if (exactRules.length) { out.exact = new RegExp('^' + _convertRulesToRegex(exactRules) + '$') } if (wildcardRules.length) { // The 'g' option is what makes the RegExp set `lastIndex` which we use to // test the strength of the match. out.wildcard = new RegExp('^' + _convertRulesToRegex(wildcardRules), 'g') } return out } /** * Converts an array of attribute rules into a regular expression string. * * `["foo.bar", "foo.bang"]` becomes "(?:foo\.(?:bar|bang))" * * @private * @param {Array.<string>} rules - The set of rules compose into a regex. * @returns {string} The rules composed into a single regular expression string. */ function _convertRulesToRegex(rules) { return ( '(?:' + rules .sort(function ruleSorter(a, b) { // Step 1) Sort the rules according to match-ability. This way the regex // will test the rules with the highest possible strength before weaker rules. if (a[a.length - 1] !== '*') { // If `a` is an exact rule, it should be moved up. return -1 } else if (b[b.length - 1] !== '*') { // If `b` is an exact rule and `a` is not, `b` should be moved up. return 1 } // Both `a` and `b` are wildcard rules, so the rule with greater length // should be moved up. return b.length - a.length }) .map(function ruleSplitter(rule) { // Step 2) Escape regex special characters and split the rules into arrays. // 'foo.bar' => ['foo', 'bar'] // 'foo.bang*' => ['foo', 'bang\\*'] // 'fizz.bang' => ['fizz', 'bang'] // '*' => ['\\*'] return rule .replace(/([.*+?|\\^$()[\]])/g, function cleaner(m) { return '\\' + m }) .split('.') }) .reduce(function ruleTransformer(collection, ruleParts) { // Step 3) Merge the split rules into a single nested array, deduplicating // rule sections as we go. // ['foo', 'bar'] => [['foo\\.', ['bar']]] // ['foo', 'bang\\*'] => [['foo\\.', ['bar'], ['bang']]] // ['fizz', 'bang'] => [['foo\\.', ['bar'], ['bang']], ['fizz\\.', ['bang']]] // ['\\*'] => [['foo\\.', ['bar'], ['bang']], ['fizz\\.', ['bang']], ['']] add(collection, ruleParts, 0) return collection function add(c, r, i) { let v = r[i] if (i !== r.length - 1) { v += '.' } else if (/\\\*$/.test(v)) { v = v.substring(0, v.length - 2) } const idx = c.findIndex(function findV(a) { return a[0] === v }) let part = c[idx] if (idx === -1) { part = [v] c.push(part) } if (i !== r.length - 1) { add(part, r, i + 1) } } }, []) .map(function rulesToRegex(part) { // Step 4) Merge each of the transformed rules into a regex. // ['foo\\.', ['bar', 'bang']] => 'foo\\.(?:bar|bang)' // ['fizz\\.', ['bang']] => 'fizz\\.(?:bang)' // [''] => '' return mapper(part) function mapper(p) { if (typeof p === 'string') { return p } else if (p.length === 1) { return mapper(p[0]) } const first = mapper(p.shift()) // shift === pop_front return first + '(?:' + p.map(mapper).join('|') + ')' } }) .join('|') + ')' ) // Step 5) Merge all the regex strings into one. } /** * Helper method for updating our list of destinations * * @private * @param {object} destinations list of destination ids * @param {string} id destination id * @param {boolean} result whether or not we found a match * @returns {object} potentially modified list of destinations */ function _updateDestinations(destinations, id, result) { if (result === NO_MATCH) { // No match, no-op. } else if (result) { destinations |= id // Positive match, add it in. } else { destinations &= ~id // Negative match, remove it. } return destinations }