Comparing version 0.9.12-92 to 0.9.13-101
@@ -160,3 +160,3 @@ 'use strict'; | ||
Agent.prototype.updateNormalizerRules = function (response) { | ||
this.normalizer.parseMetricRules(response); | ||
this.normalizer.load(response); | ||
}; | ||
@@ -163,0 +163,0 @@ |
@@ -13,5 +13,19 @@ 'use strict'; | ||
var PROTOCOL_VERSION = 9; | ||
/* | ||
* CONSTANTS | ||
*/ | ||
var PROTOCOL_VERSION = 9 | ||
, RESPONSE_VALUE_NAME = 'return_value' | ||
, RUN_ID_NAME = 'agent_run_id' | ||
, RAW_METHOD_PATH = '/agent_listener/invoke_raw_method' | ||
// https://hudson.newrelic.com/job/collector-master/javadoc/com/nr/servlet/AgentListener.html | ||
, USER_AGENT_FORMAT = "NewRelic-NodeAgent/%s (nodejs %s %s-%s)" | ||
, ENCODING_HEADER = 'CONTENT-ENCODING' | ||
, CONTENT_TYPE_HEADER = 'Content-Type' | ||
, DEFAULT_ENCODING = 'identity' | ||
, COMPRESSED_ENCODING = 'deflate' | ||
, DEFAULT_CONTENT_TYPE = 'application/json' | ||
, COMPRESSED_CONTENT_TYPE = 'application/octet-stream' | ||
; | ||
// FIXME support proxies | ||
// TODO add configurable timeout to connections | ||
@@ -24,156 +38,236 @@ function DataSender(config, agentRunId) { | ||
this._url = url.format({ | ||
pathname : '/agent_listener/invoke_raw_method', | ||
query : { | ||
marshal_format : 'json', | ||
protocol_version : PROTOCOL_VERSION, | ||
license_key : this.config.license_key | ||
} | ||
}); | ||
this.on('error', function (method, error) { | ||
var hostname = this.config.proxy_host || this.config.host; | ||
if (typeof error === 'object') { | ||
if (error.error_type !== 'NewRelic::Agent::ForceRestartException') { | ||
logger.debug("Attempting to send data to collector %s failed:", hostname); | ||
logger.debug(error); | ||
} | ||
} | ||
else { | ||
logger.debug("Attempting to send data to collector %s failed: %j", hostname, error); | ||
} | ||
}.bind(this)); | ||
this.on('error', this.onError.bind(this)); | ||
} | ||
util.inherits(DataSender, events.EventEmitter); | ||
DataSender.prototype.getUserAgent = function () { | ||
// as per https://hudson.newrelic.com/job/collector-master/javadoc/com/nr/servlet/AgentListener.html | ||
return util.format("NewRelic-NodeAgent/%s (nodejs %s %s-%s)", | ||
this.config.version, | ||
process.versions.node, | ||
process.platform, | ||
process.arch); | ||
}; | ||
/** | ||
* The primary interface to DataSender objects. If you're calling anything on | ||
* DataSender objects aside from invokeMethod (and you're not writing test | ||
* code), something is probably awry. | ||
* | ||
* @param string message The type of message you want to send the collector. | ||
* @param object data Serializable data to be sent. | ||
*/ | ||
DataSender.prototype.invokeMethod = function (message, body) { | ||
if (!message) throw new Error("Can't send the collector a message without a message type."); | ||
DataSender.prototype.canonicalizeURL = function (url) { | ||
if (this.config.proxy_host) { | ||
return 'http://' + this.config.host + ':' + this.config.port + url; | ||
var data = JSON.stringify(body || []); | ||
if (this.shouldCompress(data)) { | ||
logger.trace("Data[%s] (COMPRESSED): %s", message, (data || "(no data)")); | ||
zlib.deflate(data, function (err, deflated) { | ||
if (err) return logger.verbose(err, "Error compressing JSON for delivery. Not sending."); | ||
var headers = this.getHeaders(deflated, true); | ||
this.postToCollector(message, headers, deflated); | ||
}.bind(this)); | ||
} | ||
else { | ||
return url; | ||
logger.trace("Data[%s]: %s", message, (data || "(no data)")); | ||
var headers = this.getHeaders(data, false); | ||
// ensure that invokeMethod is always asynchronous | ||
process.nextTick(this.postToCollector.bind(this, message, headers, data)); | ||
} | ||
}; | ||
DataSender.prototype.createHeaders = function (length, compressed) { | ||
return { | ||
"CONTENT-ENCODING" : compressed ? 'deflate' : 'identity', | ||
"Content-Length" : length, | ||
"Connection" : "Keep-Alive", | ||
"host" : this.config.host, | ||
"Content-Type" : compressed ? 'application/octet-stream' : 'application/json', | ||
"User-Agent" : this.getUserAgent() | ||
}; | ||
}; | ||
/** | ||
* Send a message to the collector and set up the sender to handle the | ||
* result. | ||
* | ||
* @param string message The message type being sent to the collector. | ||
* @param object headers The headers for this request. | ||
* @param string data The encoded data, ready for delivery. | ||
*/ | ||
DataSender.prototype.postToCollector = function (message, headers, data) { | ||
var port = this.config.proxy_port || this.config.port | ||
, host = this.config.proxy_host || this.config.host | ||
, urlPath = this.getURL(message) | ||
; | ||
DataSender.prototype.createRequest = function (method, url, headers) { | ||
logger.debug("Posting %s message to %s:%s at %s.", message, host, port, urlPath); | ||
var request = http.request({ | ||
__NR__connection : true, // who measures the metrics measurer? | ||
method : 'POST', | ||
port : this.config.proxy_port || this.config.port, | ||
host : this.config.proxy_host || this.config.host, | ||
path : this.canonicalizeURL(url), | ||
port : port, | ||
host : host, | ||
path : urlPath, | ||
headers : headers | ||
}); | ||
request.on('error', function (error) { | ||
logger.info(error, "Error invoking %s method.", method); | ||
this.emit('error', method, error); | ||
}.bind(this)); | ||
request.on('error', this.onError.bind(this, message)); | ||
request.on('response', this.onCollectorResponse.bind(this, message)); | ||
request.on('response', function (response) { | ||
var VALUE = 'return_value'; | ||
return request.end(data); | ||
}; | ||
if (response.statusCode < 200 || response.statusCode >= 300) { | ||
return this.emit('error', method, response.statusCode); | ||
} | ||
/** | ||
* ForceRestartExceptions aren't actually errors, don't log them as such. | ||
*/ | ||
DataSender.prototype.onError = function (message, error) { | ||
var hostname = this.config.proxy_host || this.config.host; | ||
if (error.error_type !== 'NewRelic::Agent::ForceRestartException') { | ||
logger.debug(error, | ||
"Attempting to send %s to collector %s failed:", | ||
message, | ||
hostname); | ||
} | ||
}; | ||
// if the encoding isn't explicitly set on the response, the chunks will | ||
// be Buffers and not strings. | ||
response.setEncoding('utf8'); | ||
response.pipe(new StreamSink(function (error, body) { | ||
if (error) return this.emit('error', method, response); | ||
/** | ||
* Pipe the data from the collector to a response handler. | ||
* | ||
* @param string message The message being sent to the collector. | ||
*/ | ||
DataSender.prototype.onCollectorResponse = function (message, response) { | ||
response.on('end', function () { | ||
logger.debug("Finished receiving data back from the collector for %s.", message); | ||
}); | ||
var message = JSON.parse(body); | ||
if (response.statusCode < 200 || response.statusCode >= 300) { | ||
logger.error("Got %s as a response code from the collector.", | ||
response.statusCode); | ||
return this.emit('error', | ||
message, | ||
new Error(util.format("Got HTTP %s in response to %s.", | ||
response.statusCode, | ||
message))); | ||
} | ||
// can be super verbose, but save as a raw object anyway | ||
logger.trace({response : message}, "parsed response from collector:"); | ||
if (message.exception) return this.emit('error', method, message.exception); | ||
response.setEncoding('utf8'); | ||
response.pipe(new StreamSink(this.handleMessage.bind(this, message))); | ||
}; | ||
// if we get messages back from the collector, be polite and pass them along | ||
if (message[VALUE] && message[VALUE].messages) { | ||
var messages = message[VALUE].messages; | ||
messages.forEach(function (message) { logger.info(message.message); }); | ||
} | ||
/** | ||
* Responses from the collector follow the convention: | ||
* | ||
* { exception : { <exception JSON> }, | ||
* return_value : { | ||
* messages : [ <messages> ], | ||
* <other stuff> | ||
* } | ||
* } | ||
* | ||
* Exceptions are emitted as-is, as errors. | ||
* Anything associated with return_value is emitted as a response on | ||
* the DataSender. | ||
* | ||
* @param string message The message type sent to the collector. | ||
* @param error error The error, if any, resulting from decoding the | ||
* response. | ||
* @param string body The body of the response. | ||
*/ | ||
DataSender.prototype.handleMessage = function (message, error, body) { | ||
if (error) return this.emit('error', message, error); | ||
this.emit('response', message[VALUE]); | ||
}.bind(this))); | ||
}.bind(this)); | ||
var json = JSON.parse(body); | ||
// can be super verbose, but useful for debugging | ||
logger.trace({response : json}, "parsed response from collector:"); | ||
return request; | ||
}; | ||
// If we get messages back from the collector, be polite and pass them along. | ||
var returned = json[RESPONSE_VALUE_NAME]; | ||
if (returned && returned.messages) { | ||
returned.messages.forEach(function (element) { | ||
logger.info(element.message); | ||
}); | ||
} | ||
DataSender.prototype.sendPlaintext = function sendPlainText(method, url, data) { | ||
var contentLength = Buffer.byteLength(data, 'utf8') | ||
, headers = this.createHeaders(contentLength) | ||
; | ||
/* If there's an exception, wait to return it until any messages have | ||
* been passed along. | ||
*/ | ||
if (json.exception) { | ||
return this.emit('error', message, json.exception); | ||
} | ||
logger.trace("Headers: %j", headers); | ||
logger.debug("Data[%s]: %s", method, (data || "(no data)")); | ||
return this.emit('response', returned); | ||
}; | ||
var request = this.createRequest(method, url, headers); | ||
request.write(data); | ||
request.end(); | ||
/** | ||
* See the constants list for the format string (and the URL that explains it). | ||
*/ | ||
DataSender.prototype.getUserAgent = function () { | ||
return util.format(USER_AGENT_FORMAT, | ||
this.config.version, | ||
process.versions.node, | ||
process.platform, | ||
process.arch); | ||
}; | ||
DataSender.prototype.sendCompressed = function sendCompressed(method, url, data) { | ||
zlib.deflate(data, function (err, deflated) { | ||
if (err) return logger.verboe(err, "Error compressing JSON for delivery."); | ||
/** | ||
* This method implies proxy support, but it's completely untested | ||
* (and mostly undocumented in the config). | ||
* | ||
* FIXME tested, more robust proxy support | ||
* FIXME use the newer "RESTful" URLs | ||
* | ||
* @param string message The message type sent to the collector. | ||
* | ||
* @returns string The URL path to be POSTed to. | ||
*/ | ||
DataSender.prototype.getURL = function (message) { | ||
var query = { | ||
marshal_format : 'json', | ||
protocol_version : PROTOCOL_VERSION, | ||
license_key : this.config.license_key, | ||
method : message | ||
}; | ||
var contentLength = deflated.length | ||
, headers = this.createHeaders(contentLength, true) // cmprssd plz | ||
; | ||
if (this.agentRunId) query[RUN_ID_NAME] = this.agentRunId; | ||
logger.debug("Headers: %s", util.inspect(headers)); | ||
logger.debug("Data[%s] (COMPRESSED): %s", method, (data || "(no data)")); | ||
var formatted = url.format({ | ||
pathname : RAW_METHOD_PATH, | ||
query : query | ||
}); | ||
var request = this.createRequest(method, url, headers); | ||
request.write(deflated); | ||
request.end(); | ||
}.bind(this)); | ||
if (this.config.proxy_host) { | ||
return 'http://' + this.config.host + ':' + this.config.port + formatted; | ||
} | ||
else { | ||
return formatted; | ||
} | ||
}; | ||
DataSender.prototype.send = function (method, url, params) { | ||
logger.debug("Posting to %s.", url); | ||
/** | ||
* | ||
* @param string data (Potentially compressed) data to be sent to | ||
* collector. | ||
* @param boolean compressed Whether the data are compressed. | ||
*/ | ||
DataSender.prototype.getHeaders = function (data, compressed) { | ||
var length = Buffer.byteLength(data, 'utf8') | ||
, agent = this.getUserAgent() | ||
; | ||
if (!params) params = []; | ||
var data = JSON.stringify(params); | ||
var headers = { | ||
'User-Agent' : agent, | ||
'Connection' : 'Keep-Alive', | ||
// select the virtual host on the server end | ||
'host' : this.config.host, | ||
'Content-Length' : length, | ||
}; | ||
var contentLength = Buffer.byteLength(data, 'utf8'); | ||
if (contentLength > 65536) { | ||
this.sendCompressed(method, url, data); | ||
if (compressed) { | ||
headers[ENCODING_HEADER] = COMPRESSED_ENCODING; | ||
headers[CONTENT_TYPE_HEADER] = COMPRESSED_CONTENT_TYPE; | ||
} | ||
else { | ||
this.sendPlaintext(method, url, data); | ||
headers[ENCODING_HEADER] = DEFAULT_ENCODING; | ||
headers[CONTENT_TYPE_HEADER] = DEFAULT_CONTENT_TYPE; | ||
} | ||
return headers; | ||
}; | ||
DataSender.prototype.invokeMethod = function (method, params) { | ||
var url = this._url + "&method=" + method; | ||
if (this.agentRunId) url += "&agent_run_id=" + this.agentRunId; | ||
this.send(method, url, params); | ||
/** | ||
* FLN pretty much decided on his own recognizance that 64K was a good point | ||
* at which to compress a server response. There's only a loose consensus that | ||
* the threshold should probably be much higher than this, if only to keep the | ||
* load on the collector down. | ||
* | ||
* FIXME: come up with a better heuristic | ||
*/ | ||
DataSender.prototype.shouldCompress = function (data) { | ||
return data && Buffer.byteLength(data, 'utf8') > 65536; | ||
}; | ||
module.exports = DataSender; |
'use strict'; | ||
var path = require('path') | ||
, util = require('util') | ||
, logger = require(path.join(__dirname, '..', 'logger')).child({component : 'metric_normalizer'}) | ||
, Rule = require(path.join(__dirname, 'normalizer', 'rule')) | ||
var path = require('path') | ||
, util = require('util') | ||
, logger = require(path.join(__dirname, '..', 'logger')).child({component : 'metric_normalizer'}) | ||
, deepEqual = require(path.join(__dirname, '..', 'util', 'deep-equal')) | ||
, Rule = require(path.join(__dirname, 'normalizer', 'rule')) | ||
; | ||
function MetricNormalizer(rules) { | ||
if (rules) this.parseMetricRules(rules); | ||
/** | ||
* The collector keeps track of rules that should be applied to metric names, | ||
* and sends these rules to the agent at connection time. These rules can | ||
* either change the name of the metric or indicate that metrics associated with | ||
* this name (which is generally a URL path) should be ignored altogether. | ||
*/ | ||
function MetricNormalizer(json) { | ||
if (json) this.load(json); | ||
} | ||
MetricNormalizer.prototype.parseMetricRules = function (connectResponse) { | ||
if (connectResponse && connectResponse.url_rules) { | ||
logger.debug("Received %d metric normalization rule(s)", | ||
connectResponse.url_rules.length); | ||
/** | ||
* Convert the raw, deserialized JSON response into a set of | ||
* NormalizationRules. | ||
* | ||
* FIXME: dedupe the rule list after sorting. | ||
* | ||
* @param object json The deserialized JSON response sent on collector | ||
* connection. | ||
*/ | ||
MetricNormalizer.prototype.load = function (json) { | ||
if (json && json.url_rules) { | ||
var raw = json.url_rules; | ||
logger.debug("Received %d metric normalization rule(s)", raw.length); | ||
if (!this.rules) this.rules = []; | ||
connectResponse.url_rules.forEach(function (ruleJSON) { | ||
this.rules.push(new Rule(ruleJSON)); | ||
raw.forEach(function (json) { | ||
var rule = new Rule(json); | ||
// no need to add the same rule twice | ||
if (!this.rules.some(function (r) { return deepEqual(r, rule); })) { | ||
this.rules.push(rule); | ||
} | ||
}.bind(this)); | ||
// I (FLN) always forget this, so making a note: | ||
// JS sort is always IN-PLACE. | ||
this.rules.sort(function (a, b) { | ||
return a.precedence - b.precedence; | ||
}); | ||
/* I (FLN) always forget this, so making a note: JS sort is always | ||
* IN-PLACE, even though it returns the sorted array. | ||
*/ | ||
this.rules.sort(function (a, b) { return a.precedence - b.precedence; }); | ||
logger.debug("Normalized to %s metric normalization rule(s).", this.rules.length); | ||
} | ||
}; | ||
MetricNormalizer.prototype.normalizeUrl = function (url) { | ||
if (!this.rules) return null; | ||
/** | ||
* Returns an object with these properties: | ||
* | ||
* 1. name: the raw name | ||
* 2. normalized: the normalized name (if applicable) | ||
* 3. ignore: present and true if the matched rule says to ignore matching names | ||
* 4. terminal: present and true if the matched rule terminated evaluation | ||
*/ | ||
MetricNormalizer.prototype.normalize = function (name) { | ||
var result = {name : name}; | ||
var normalized = url; | ||
var isNormalized = false; | ||
if (!this.rules || this.rules.length === 0) return result; | ||
var last = name | ||
, normalized | ||
; | ||
for (var i = 0; i < this.rules.length; i++) { | ||
if (this.rules[i].matches(normalized)) { | ||
/* | ||
* It's possible for normalization rules to match without transforming. | ||
* | ||
* Don't assume that it's required for the URL to actually change | ||
* for normalization to have taken place. | ||
*/ | ||
isNormalized = true; | ||
normalized = this.rules[i].apply(normalized); | ||
// assume that terminate_chain only applies upon match | ||
if (this.rules[i].isTerminal) break; | ||
var rule = this.rules[i]; | ||
if (rule.matches(last)) { | ||
result.normalized = rule.apply(last); | ||
if (rule.ignore) { | ||
result.ignore = true; | ||
delete result.normalized; | ||
logger.trace(rule, "Ignoring %s because of rule:", name); | ||
break; | ||
} | ||
logger.trace(rule, "Normalized %s to %s because of rule:", | ||
last, result.normalized); | ||
if (rule.isTerminal) { | ||
result.terminal = true; | ||
logger.trace(rule, "Terminating normalization because of rule:"); | ||
break; | ||
} | ||
last = result.normalized; | ||
} | ||
} | ||
if (!isNormalized) return null; | ||
return normalized; | ||
return result; | ||
}; | ||
module.exports = MetricNormalizer; |
'use strict'; | ||
var path = require('path') | ||
var path = require('path') | ||
, logger = require(path.join(__dirname, '..', '..', 'logger')).child({component : 'normalizer_rule'}) | ||
@@ -32,3 +32,2 @@ ; | ||
this.isTerminal = json.terminate_chain || false; | ||
this.patternString = json.match_expression || '^$'; | ||
this.replacement = replaceReplacer(json.replacement || '$0'); | ||
@@ -43,4 +42,4 @@ this.replaceAll = json.replace_all || false; | ||
try { | ||
this.pattern = new RegExp(this.patternString, modifiers); | ||
logger.trace("Parsed URL normalization rule with pattern %s.", this.pattern); | ||
this.pattern = new RegExp(json.match_expression || '^$', modifiers); | ||
logger.trace("Loaded normalization rule: %j", this); | ||
} | ||
@@ -91,2 +90,8 @@ catch (error) { | ||
.map(function (segment) { | ||
/* String.split will return empty segments when the path has a leading | ||
* slash or contains a run of slashes. Don't inadvertently substitute or | ||
* drop these empty segments, or the normalized path will be wrong. | ||
*/ | ||
if (segment === "") return segment; | ||
return segment.replace(this.pattern, this.replacement); | ||
@@ -93,0 +98,0 @@ }.bind(this)) |
@@ -115,5 +115,7 @@ 'use strict'; | ||
if (this.trace && (duration || duration === 0)) this.trace.setDurationInMillis(duration); | ||
if (this.trace && (duration || duration === 0)) { | ||
this.trace.setDurationInMillis(duration); | ||
} | ||
var partialName; | ||
var name, partialName; | ||
if (statusCode === 414 || // Request-URI Too Long | ||
@@ -131,5 +133,5 @@ (statusCode >= 400 && statusCode < 405)) { | ||
var normalizedUrl = this.normalizer.normalizeUrl(this.url); | ||
if (normalizedUrl) { | ||
partialName = 'NormalizedUri' + normalizedUrl; | ||
name = this.normalizer.normalize(this.url); | ||
if (name.normalized) { | ||
partialName = 'NormalizedUri' + name.normalized; | ||
} | ||
@@ -141,20 +143,26 @@ else { | ||
var metrics = this.metrics; | ||
metrics.measureDurationUnscoped('WebTransaction', duration); | ||
metrics.measureDurationUnscoped('HttpDispatcher', duration); | ||
// normalization rules tell us to ignore certain metrics | ||
if (name && name.ignore) { | ||
this.ignore = true; | ||
} | ||
else { | ||
var metrics = this.metrics; | ||
metrics.measureDurationUnscoped('WebTransaction', duration); | ||
metrics.measureDurationUnscoped('HttpDispatcher', duration); | ||
this.scope = "WebTransaction/" + partialName; | ||
// var maxDuration = Math.max(0, duration - this.totalExclusive); | ||
var maxDuration = Math.max(0, exclusiveDuration); | ||
metrics.measureDurationUnscoped(this.scope, duration, maxDuration); | ||
this.scope = "WebTransaction/" + partialName; | ||
// var maxDuration = Math.max(0, duration - this.totalExclusive); | ||
var maxDuration = Math.max(0, exclusiveDuration); | ||
metrics.measureDurationUnscoped(this.scope, duration, maxDuration); | ||
['Apdex/' + partialName, 'Apdex'].forEach(function (name) { | ||
var apdexStats = metrics.getOrCreateApdexMetric(name).stats; | ||
if (isError) { | ||
apdexStats.incrementFrustrating(); | ||
} | ||
else { | ||
apdexStats.recordValueInMillis(duration); | ||
} | ||
}); | ||
['Apdex/' + partialName, 'Apdex'].forEach(function (name) { | ||
var apdexStats = metrics.getOrCreateApdexMetric(name).stats; | ||
if (isError) { | ||
apdexStats.incrementFrustrating(); | ||
} | ||
else { | ||
apdexStats.recordValueInMillis(duration); | ||
} | ||
}); | ||
} | ||
@@ -161,0 +169,0 @@ return this.scope; |
@@ -69,3 +69,3 @@ 'use strict'; | ||
TraceAggregator.prototype.add = function add(transaction) { | ||
if (transaction && transaction.metrics) { | ||
if (transaction && transaction.metrics && !transaction.ignore) { | ||
var trace = transaction.getTrace() | ||
@@ -72,0 +72,0 @@ , scope = transaction.scope |
@@ -71,3 +71,11 @@ 'use strict'; | ||
Object.keys(parsed.query).forEach(function (key) { | ||
if (parsed.query[key] === '') { | ||
/* 'var1&var2=value' is not necessarily the same as 'var1=&var2=value' | ||
* | ||
* In my world, one is an assertion of presence, and the other is | ||
* an empty variable. Some web frameworks behave this way as well, | ||
* so don't lose information. | ||
* | ||
* TODO: figure out if this confuses everyone and remove if so. | ||
*/ | ||
if (parsed.query[key] === '' && parsed.path.indexOf(key + '=') < 0) { | ||
segment.parameters[key] = true; | ||
@@ -74,0 +82,0 @@ } |
10
NEWS.md
@@ -0,1 +1,11 @@ | ||
### v0.9.13-101 / beta-13 (2013-01-07): | ||
* When New Relic's servers (or an intermediate proxy) returned a response with | ||
a status code other than 20x, the entire instrumented application would | ||
crash. | ||
* Some metric normalization rules were not being interpreted correctly, leading | ||
to malformed normalized metric names. | ||
* Metric normalization rules that specifed that matching metrics were to be | ||
ignored were not being enforced. | ||
### v0.9.12-91 / beta-12 (2012-12-28): | ||
@@ -2,0 +12,0 @@ |
{ | ||
"name": "newrelic", | ||
"version": "0.9.12-92", | ||
"version": "0.9.13-101", | ||
"author": "New Relic Node.js agent team <nodejs@newrelic.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -11,5 +11,5 @@ # New Relic Node.js agent | ||
1. [Install node](http://nodejs.org/#download). For now, at least 0.6 is | ||
required. Some features (e.g. error tracing) depend in whole or in part on | ||
features in 0.8 and above. Development work is being done against the latest | ||
released version. | ||
required. Some features (e.g. error tracing) depend in whole or in | ||
part on features in 0.8 and above. Development work on the agentis | ||
being done against the latest released non-development version of Node. | ||
2. Install this module via `npm install newrelic` for the application you | ||
@@ -29,18 +29,19 @@ want to monitor. | ||
When you start your app, the agent should start up with it and start reporting | ||
data that will appear within our UI after a few minutes. Because the agent | ||
minimizes the amount of bandwidth it consumes, it only reports metrics, errors | ||
and transaction traces once a minute, so if you add the agent to tests that run | ||
in under a minute, the agent won't have time to report data to New Relic. The | ||
agent will write its log to a file named `newrelic_agent.log` in the | ||
application directory. If the agent doesn't send data or crashes your app, the | ||
log can help New Relic determine what went wrong, so be sure to send it along | ||
with any bug reports or support requests. | ||
When you start your app, the agent should start up with it and start | ||
reporting data that will appear within our UI after a few minutes. | ||
Because the agent minimizes the amount of bandwidth it consumes, it | ||
only reports data once a minute, so if you add the agent to tests | ||
that take less than a minute to run, the agent won't have time to | ||
report data to New Relic. The agent will write its log to a file named | ||
`newrelic_agent.log` in the application directory. If the agent doesn't | ||
send data or crashes your app, the log can help New Relic determine what | ||
went wrong, so be sure to send it along with any bug reports or support | ||
requests. | ||
## Configuring the agent | ||
The agent can be tailored to your app's requirements, both from the server and | ||
via the newrelic.js configuration file you created above. For more details on | ||
what can be configured, refer to `lib/config.default.js`, which documents | ||
the available variables and their default values. | ||
The agent can be tailored to your app's requirements, both from the | ||
server and via the newrelic.js configuration file you created. For more | ||
details on what can be configured, refer to `lib/config.default.js`, | ||
which documents the available variables and their default values. | ||
@@ -47,0 +48,0 @@ In addition, for those of you running in Heroku, Microsoft Azure or any other |
@@ -65,7 +65,6 @@ 'use strict'; | ||
var sender = new DataSender(agent.config, SAMPLE_RUN_ID); | ||
// stub out DataSender.send | ||
sender.send = function (sMethod, sUri, sParams) { | ||
method = sMethod; | ||
uri = sUri; | ||
params = sParams; | ||
sender.invokeMethod = function (sMethod, sParams) { | ||
method = sMethod; | ||
uri = sender.getURL(method); | ||
params = sParams; | ||
}; | ||
@@ -72,0 +71,0 @@ |
'use strict'; | ||
var path = require('path') | ||
, chai = require('chai') | ||
, expect = chai.expect | ||
, sinon = require('sinon') | ||
, DataSender = require(path.join(__dirname, '..', 'lib', 'collector', 'data-sender')) | ||
var path = require('path') | ||
, chai = require('chai') | ||
, expect = chai.expect | ||
, should = chai.should() | ||
, EventEmitter = require('events').EventEmitter | ||
, Stream = require('stream') | ||
, DataSender = require(path.join(__dirname, '..', 'lib', 'collector', 'data-sender')) | ||
; | ||
@@ -28,7 +30,15 @@ | ||
var raw = '/listener/invoke'; | ||
var expected = 'http://collector.newrelic.com:80/listener/invoke'; | ||
expect(sender.canonicalizeURL(raw)).equal(expected); | ||
var expected = 'http://collector.newrelic.com:80' + | ||
'/agent_listener/invoke_raw_method' + | ||
'?marshal_format=json&protocol_version=9&' + | ||
'license_key=&method=test&agent_run_id=12'; | ||
expect(sender.getURL('test')).equal(expected); | ||
}); | ||
it("should require a message type when invoking a remote method", function () { | ||
var sender = new DataSender(); | ||
expect(function () { sender.invokeMethod(); }) | ||
.throws("Can't send the collector a message without a message type"); | ||
}); | ||
describe("when generating headers for a plain request", function () { | ||
@@ -44,3 +54,3 @@ var headers; | ||
headers = sender.createHeaders(80); | ||
headers = sender.getHeaders('test'); | ||
}); | ||
@@ -53,3 +63,3 @@ | ||
it("should use the content length from the parameter", function () { | ||
expect(headers["Content-Length"]).equal(80); | ||
expect(headers["Content-Length"]).equal(4); | ||
}); | ||
@@ -84,3 +94,3 @@ | ||
headers = sender.createHeaders(92, true); | ||
headers = sender.getHeaders('zxxvxzxzzx', true); | ||
}); | ||
@@ -93,3 +103,3 @@ | ||
it("should use the content length from the parameter", function () { | ||
expect(headers["Content-Length"]).equal(92); | ||
expect(headers["Content-Length"]).equal(10); | ||
}); | ||
@@ -114,5 +124,4 @@ | ||
describe("when performing RPC against the collector", function () { | ||
describe("when generating the collector URL", function () { | ||
var sender | ||
, mockSender | ||
, TEST_RUN_ID = Math.floor(Math.random() * 3000) | ||
@@ -128,29 +137,99 @@ ; | ||
sender = new DataSender(config, TEST_RUN_ID); | ||
mockSender = sinon.mock(sender); | ||
}); | ||
it("should always add the agent run ID, if set", function () { | ||
var METHOD = 'TEST' | ||
, PARAMS = {test : "value"} | ||
, URL = sinon.match(new RegExp('agent_run_id=' + TEST_RUN_ID)) | ||
; | ||
expect(sender.agentRunId).equal(TEST_RUN_ID); | ||
expect(sender.getURL('TEST_METHOD')).match(new RegExp('agent_run_id=' + TEST_RUN_ID)); | ||
}); | ||
mockSender.expects('send').once().withExactArgs(METHOD, URL, PARAMS); | ||
sender.invokeMethod(METHOD, PARAMS); | ||
it("should correctly set up the method", function () { | ||
expect(sender.getURL('TEST_METHOD')).match(/method=TEST_METHOD/); | ||
}); | ||
}); | ||
mockSender.verify(); | ||
describe("when processing a collector response", function () { | ||
var sender; | ||
beforeEach(function () { | ||
sender = new DataSender({host : 'localhost'}); | ||
}); | ||
it("should correctly set up the method", function () { | ||
var METHOD = 'TEST' | ||
, PARAMS = {test : "value"} | ||
, URL = sinon.match(new RegExp('method=' + METHOD)) | ||
; | ||
it("should raise an error if the response has an error status code", function (done) { | ||
var response = new EventEmitter(); | ||
response.statusCode = 401; | ||
mockSender.expects('send').once().withExactArgs(METHOD, URL, PARAMS); | ||
sender.invokeMethod(METHOD, PARAMS); | ||
sender.on('error', function (message, error) { | ||
expect(error.message).equal("Got HTTP 401 in response to TEST."); | ||
mockSender.verify(); | ||
return done(); | ||
}); | ||
sender.onCollectorResponse('TEST', response); | ||
}); | ||
it("should hand off the response to the message handler", function (done) { | ||
var response = new Stream(); | ||
response.setEncoding = function () {}; // fake it til you make it | ||
response.readable = true; | ||
response.statusCode = 200; | ||
var sampleBody = '{"return_value":{"messages":[]}}'; | ||
sender.handleMessage = function (message, error, body) { | ||
expect(message).equal('TEST'); | ||
should.not.exist(error); | ||
expect(body).equal(sampleBody); | ||
return done(); | ||
}; | ||
sender.onCollectorResponse('TEST', response); | ||
process.nextTick(function () { | ||
response.emit('data', sampleBody); | ||
response.emit('end'); | ||
}); | ||
}); | ||
}); | ||
describe("when handling a response's message", function () { | ||
var sender; | ||
beforeEach(function () { | ||
sender = new DataSender({host : 'localhost'}); | ||
}); | ||
it("should hand off decoding errors", function (done) { | ||
sender.on('error', function (message, error) { | ||
expect(message).equal('TEST'); | ||
expect(error.message).equal('unspecified decoding error'); | ||
return done(); | ||
}); | ||
sender.handleMessage('TEST', new Error('unspecified decoding error')); | ||
}); | ||
it("should hand off server exceptions", function (done) { | ||
sender.on('error', function (message, error) { | ||
expect(message).equal('TEST'); | ||
expect(error).eql({error_type : 'NewRelic::Agent::ForceRestartException'}); | ||
return done(); | ||
}); | ||
var body = '{"exception":{"error_type":"NewRelic::Agent::ForceRestartException"}}'; | ||
sender.handleMessage('TEST', null, body); | ||
}); | ||
it("should hand off return_value, if set", function (done) { | ||
sender.on('response', function (response) { | ||
expect(response).eql({url_rules : []}); | ||
return done(); | ||
}); | ||
var body = '{"return_value":{"url_rules":[]}}'; | ||
sender.handleMessage('TEST', null, body); | ||
}); | ||
}); | ||
}); |
@@ -70,2 +70,43 @@ 'use strict'; | ||
describe("with Saxon's patterns", function () { | ||
describe("including '^(?!account|application).*'", function () { | ||
beforeEach(function () { | ||
rule = new Rule({ | ||
"each_segment" : true, | ||
"match_expression" : "^(?!account|application).*", | ||
"replacement" : "*" | ||
}); | ||
}); | ||
it("implies '/account/myacc/application/test' -> '/account/*/application/*'", | ||
function () { | ||
expect(rule.apply('/account/myacc/application/test')) | ||
.equal('/account/*/application/*'); | ||
}); | ||
it("implies '/oh/dude/account/myacc/application' -> '/*/*/account/*/application'", | ||
function () { | ||
expect(rule.apply('/oh/dude/account/myacc/application')) | ||
.equal('/*/*/account/*/application'); | ||
}); | ||
}); | ||
describe("including '^(?!channel|download|popups|search|tap|user|related|admin|api|genres|notification).*'", | ||
function () { | ||
beforeEach(function () { | ||
rule = new Rule({ | ||
"each_segment" : true, | ||
"match_expression" : "^(?!channel|download|popups|search|tap|user|related|admin|api|genres|notification).*", | ||
"replacement" : "*" | ||
}); | ||
}); | ||
it("implies '/tap/stuff/user/gfy77t/view' -> '/tap/*/user/*/*'", | ||
function () { | ||
expect(rule.apply('/tap/stuff/user/gfy77t/view')) | ||
.equal('/tap/*/user/*/*'); | ||
}); | ||
}); | ||
}); | ||
describe("with a more complex substitution rule", function () { | ||
@@ -144,33 +185,33 @@ before(function () { | ||
it("should default to not applying the rule to each segment", function () { | ||
expect((new Rule()).eachSegment).equal(false); | ||
expect(new Rule().eachSegment).equal(false); | ||
}); | ||
it("should default the rule's precedence to 0", function () { | ||
expect((new Rule()).precedence).equal(0); | ||
expect(new Rule().precedence).equal(0); | ||
}); | ||
it("should default to not terminating rule evaluation", function () { | ||
expect((new Rule()).isTerminal).equal(false); | ||
expect(new Rule().isTerminal).equal(false); | ||
}); | ||
it("should have a regexp that matches the empty string", function () { | ||
expect((new Rule()).patternString).equal('^$'); | ||
expect(new Rule().pattern).eql(/^$/); | ||
}); | ||
it("should use the entire match as the replacement value", function () { | ||
expect((new Rule()).replacement).equal('$0'); | ||
expect(new Rule().replacement).equal('$0'); | ||
}); | ||
it("should default to not replacing all instances", function () { | ||
expect((new Rule()).replaceAll).equal(false); | ||
expect(new Rule().replaceAll).equal(false); | ||
}); | ||
it("should default to not ignoring matching URLs", function () { | ||
expect((new Rule()).ignore).equal(false); | ||
expect(new Rule().ignore).equal(false); | ||
}); | ||
it("should silently pass through the input if applied", function () { | ||
expect((new Rule()).apply('sample/input')).equal('sample/input'); | ||
expect(new Rule().apply('sample/input')).equal('sample/input'); | ||
}); | ||
}); | ||
}); |
'use strict'; | ||
var path = require('path') | ||
, chai = require('chai') | ||
, expect = chai.expect | ||
var path = require('path') | ||
, chai = require('chai') | ||
, expect = chai.expect | ||
, Normalizer = require(path.join(__dirname, '..', 'lib', 'metrics', 'normalizer')) | ||
@@ -10,72 +10,164 @@ ; | ||
describe ("MetricNormalizer", function () { | ||
// captured from staging-collector-1.newrelic.com on 2012-08-29 | ||
var rules = | ||
[{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}]; | ||
it("shouldn't throw when instantiated without any rules", function () { | ||
expect(function () { var normalizer = new Normalizer(); }).not.throws(); | ||
}); | ||
var samples = {url_rules : rules}; | ||
it("should normalize even without any rules set", function () { | ||
expect(function () { | ||
expect(new Normalizer().normalize('/sample')).eql({name : '/sample'}); | ||
}).not.throws(); | ||
}); | ||
it("shouldn't throw when instantiated without any rules", function () { | ||
expect(function () { var normalizer = new Normalizer(); }); | ||
it("should normalize with an empty rule set", function () { | ||
expect(function () { | ||
var normalizer = new Normalizer(); | ||
normalizer.load({url_rules : []}); | ||
expect(normalizer.normalize('/sample')).eql({name : '/sample'}); | ||
}).not.throws(); | ||
}); | ||
it("shouldn't throw when instantiated with a full set of rules", function () { | ||
expect(function () { var normalizer = new Normalizer(samples); }); | ||
describe("with rules captured from the staging collector on 2012-08-29", | ||
function () { | ||
var sample = { | ||
url_rules : [ | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^(test_match_nothing)$', | ||
replace_all : false, ignore : false, replacement : '\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$', | ||
replace_all : false, ignore : false, replacement : '/*.\\1'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : '^[0-9][0-9a-f_,.-]*$', | ||
replace_all : false, ignore : false, replacement : '*'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}, | ||
{each_segment : false, eval_order : 2, terminate_chain : false, | ||
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$', | ||
replace_all : false, ignore : false, replacement : '\\1/.*\\2'} | ||
] | ||
}; | ||
it("shouldn't throw when instantiated with a full set of rules", function () { | ||
expect(function () { var normalizer = new Normalizer(sample); }).not.throws(); | ||
}); | ||
it("should eliminate duplicate rules as part of loading them", function () { | ||
var reduced = [ | ||
{eachSegment : false, precedence : 0, isTerminal : true, | ||
pattern: /^(test_match_nothing)$/, | ||
replaceAll : false, ignore : false, replacement : '$1'}, | ||
{eachSegment : false, precedence : 0, isTerminal : true, | ||
pattern: /.*\\.(css|gif|ico|jpe?g|js|png|swf)$/, | ||
replaceAll : false, ignore : false, replacement : '/*.$1'}, | ||
{eachSegment : true, precedence : 1, isTerminal : false, | ||
pattern: /^[0-9][0-9a-f_,.\-]*$/, | ||
replaceAll : false, ignore : false, replacement : '*'}, | ||
{eachSegment : false, precedence : 2, isTerminal : false, | ||
pattern: /^(.*)\/[0-9][0-9a-f_,\-]*\\.([0-9a-z][0-9a-z]*)$/, | ||
replaceAll : false, ignore : false, replacement : '$1/.*$2'} | ||
]; | ||
var normalizer = new Normalizer(sample); | ||
expect(normalizer.rules).eql(reduced); | ||
}); | ||
it("should normalize a JPEGgy URL", function () { | ||
expect(new Normalizer(sample).normalize('/excessivity.jpeg')).eql({ | ||
name : '/excessivity.jpeg', | ||
normalized : '/*.jpeg', | ||
terminal : true | ||
}); | ||
}); | ||
it("should normalize a JPGgy URL", function () { | ||
expect(new Normalizer(sample).normalize('/excessivity.jpg')).eql({ | ||
name : '/excessivity.jpg', | ||
normalized : '/*.jpg', | ||
terminal : true | ||
}); | ||
}); | ||
it("should normalize a CSS URL", function () { | ||
expect(new Normalizer(sample).normalize('/style.css')).eql({ | ||
name : '/style.css', | ||
normalized : '/*.css', | ||
terminal : true | ||
}); | ||
}); | ||
}); | ||
it("should normalize a JPEGgy URL", function () { | ||
var normalizer = new Normalizer(samples); | ||
var normalized = normalizer.normalizeUrl('/excessivity.jpeg'); | ||
expect(normalized).equal('/*.jpeg'); | ||
it("should correctly ignore a matching name", function () { | ||
var sample = { | ||
url_rules : [ | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '^/long_polling$', | ||
replace_all : false, ignore : true, replacement : '*'} | ||
] | ||
}; | ||
expect(new Normalizer(sample).normalize('/long_polling')).eql({ | ||
name : '/long_polling', | ||
ignore : true | ||
}); | ||
}); | ||
it("should normalize a JPGgy URL", function () { | ||
var normalizer = new Normalizer(samples); | ||
var normalized = normalizer.normalizeUrl('/excessivity.jpg'); | ||
expect(normalized).equal('/*.jpg'); | ||
it("should apply rules by precedence", function () { | ||
var sample = { | ||
url_rules : [ | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : 'mochi', | ||
replace_all : false, ignore : false, replacement : 'millet'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : false, | ||
match_expression : '/rice$', | ||
replace_all : false, ignore : false, replacement : '/mochi'} | ||
] | ||
}; | ||
expect(new Normalizer(sample).normalize('/rice/is/not/rice')).eql({ | ||
name : '/rice/is/not/rice', | ||
normalized : '/rice/is/not/millet' | ||
}); | ||
}); | ||
it("should normalize a CSS URL", function () { | ||
var normalizer = new Normalizer(samples); | ||
var normalized = normalizer.normalizeUrl('/style.css'); | ||
expect(normalized).equal('/*.css'); | ||
it("should terminate when indicated by rule", function () { | ||
var sample = { | ||
url_rules : [ | ||
{each_segment : true, eval_order : 1, terminate_chain : false, | ||
match_expression : 'mochi', | ||
replace_all : false, ignore : false, replacement : 'millet'}, | ||
{each_segment : false, eval_order : 0, terminate_chain : true, | ||
match_expression : '/rice$', | ||
replace_all : false, ignore : false, replacement : '/mochi'} | ||
] | ||
}; | ||
expect(new Normalizer(sample).normalize('/rice/is/not/rice')).eql({ | ||
name : '/rice/is/not/rice', | ||
normalized : '/rice/is/not/mochi', | ||
terminal : true | ||
}); | ||
}); | ||
it("should apply rules by precedence"); | ||
it("should terminate when indicated by rule"); | ||
it("should use precedence to decide how to apply multiple rules"); | ||
}); |
@@ -66,3 +66,3 @@ 'use strict'; | ||
var segment = new TraceSegment(new Trace('Test/TraceExample03'), 'UnitTest'); | ||
webChild = segment.addWeb('/test?test1=value1&test2&test3=50'); | ||
webChild = segment.addWeb('/test?test1=value1&test2&test3=50&test4='); | ||
@@ -86,6 +86,10 @@ trace.setDurationInMillis(1, 0); | ||
it("should set parameters with empty values to true", function () { | ||
it("should set bare parameters to true (as in present)", function () { | ||
expect(webChild.parameters.test2).equal(true); | ||
}); | ||
it("should set parameters with empty values to ''", function () { | ||
expect(webChild.parameters.test4).equal(''); | ||
}); | ||
it("should serialize the segment with the parameters", function () { | ||
@@ -96,3 +100,3 @@ var expected = [ | ||
'WebTransaction/Uri/test', | ||
{test1 : 'value1', test2 : true, test3 : '50'}, | ||
{test1 : 'value1', test2 : true, test3 : '50', test4 : ''}, | ||
[] | ||
@@ -99,0 +103,0 @@ ]; |
25
TODO.md
### KNOWN ISSUES: | ||
* The metric names displayed in New Relic are a work in progress. The | ||
flexibility of Node's HTTP handling and routing presents unique | ||
challenges to the New Relic data model. We're working on a set of | ||
strategies to improve how metrics are named, but be aware that metric | ||
names may change over time as these strategies are implemented. | ||
* There are irregularities around transaction trace capture and display. | ||
If you notice missing or incorrect information from transaction traces, | ||
let us know. | ||
* There are over 20,000 modules on npm. We can only instrument a tiny | ||
number of them. Even for the modules we support, there are a very | ||
large number of ways to use them. If you see data you don't expect on | ||
New Relic and have the time to produce a reduced version of the code | ||
that is producing the strange data, it will be gratefully used to | ||
improve the agent. | ||
* There is an error tracer in the Node agent, but it's a work in progress. | ||
In particular, it still does not intercept errors that may already be | ||
handled by frameworks. Also, parts of it depend on the new, experimental | ||
[domain](http://nodejs.org/api/domain.html) API added in Node 0.8, and | ||
domain-specific functionality will not work in apps running in | ||
Node 0.6.x. | ||
* The CPU and memory overhead incurred by the Node agent is relatively | ||
@@ -11,6 +31,2 @@ minor (~1-10%, depending on how much of the instrumentation your | ||
applications. | ||
* There are irregularities around transaction trace capture and display. | ||
If you notice missing or incorrect information from transaction traces, | ||
let us know. If possible, include the package.json for your application | ||
with your report. | ||
* The agent works only with Node.js 0.6 and newer. | ||
@@ -26,2 +42,3 @@ * When using Node's included clustering support, each worker process will | ||
2. CouchDB (not pre-GA) | ||
* Log rotation for the agent log. | ||
* Better tests for existing instrumentation. | ||
@@ -28,0 +45,0 @@ * Differentiate between HTTP and HTTPS connections. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
684957
280
15956
121
267