UNPKG

newrelic

Version:
413 lines (366 loc) 13 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' var NO_MATCH = -Infinity var EXACT_MATCH = Infinity var 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 {Config} 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 var 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. * * @return {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. * * @return {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. * * @return {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. * * @return {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. var globalInclude = null var globalExclude = null // Iterate over each destination and see if the rules apply. for (var i = 0; i < scope.length; ++i) { var dest = scope[i] var destId = dest.id var destName = dest.name if (!(this._enabledDestinations & destId)) { destinations &= ~destId // Remove this destination. continue } // Check for a cached result for this key. var 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. var result = _doTest(globalInclude, globalExclude, this._rules[destName], key) if (this._cachedCount < this.config.attributes.filter_cache_limit) { this._cache[destName][key] = result ++this._cachedCount } } if (result === NO_MATCH) { // No match, no-op. } else if (result) { destinations |= destId // Positive match, add it in. } else { destinations &= ~destId // Negative match, remove it. } } 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) { var 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 * * @return {bool|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 } var destExclude = _matchRules(destConfig.exclude, key) if (destExclude === EXACT_MATCH) { return false } // Then check for inclusion of the attribute. if (globalInclude === EXACT_MATCH) { return true } var 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. * * @private * * 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. * * @param {array.<string>} rules - The set of rules to match against. * @param {string} key - The name of the attribute to look for. * * @return {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 } var 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. * * @return {object} An object with `exact` and `wildcard` properties which are * `RegExp` instances for testing keys. */ function _importRules(rules) { var out = { exact: null, wildcard: null } var exactRules = [] var 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. * * @private * * `["foo.bar", "foo.bang"]` becomes "(?:foo\.(?:bar|bang))" * * @param {array.<string>} rules - The set of rules compose into a regex. * * @return {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) { var v = r[i] if (i !== r.length - 1) { v += '.' } else if (/\\\*$/.test(v)) { v = v.substr(0, v.length - 2) } var idx = c.findIndex(function findV(a) { return a[0] === v }) var 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]) } var first = mapper(p.shift()) // shift === pop_front return first + '(?:' + p.map(mapper).join('|') + ')' } }).join('|') + ')' // Step 5) Merge all the regex strings into one. }