diff --git a/config.js b/config.js index fd71ec9c1..2a7656acf 100644 --- a/config.js +++ b/config.js @@ -60,6 +60,13 @@ module.exports = { // The number of transactions we buffer before we publish to the trace // API, unless we hit `flushDelaySeconds` first. - bufferSize: 1000 + bufferSize: 1000, + + // Specifies the behavior of the trace agent in the case of an uncaught exception. + // Possible values are: + // `ignore`: Take no action. + // `flush`: Attempt to publish traces silence the exception. + // `flushAndExit`: Attempt to publish traces and exit the process. + onUncaughtException: 'ignore' } }; diff --git a/index.js b/index.js index 94dd77bc0..29bafa6ca 100644 --- a/index.js +++ b/index.js @@ -39,6 +39,8 @@ for (var i = 0; i < filesLoadedBeforeTrace.length; i++) { } } +var onUncaughtExceptionValues = ['ignore', 'flush', 'flushAndExit']; + /** * Phantom implementation of the trace agent. This allows API users to decouple * the enable/disable logic from the calls to the tracing API. The phantom API @@ -133,6 +135,12 @@ var publicAgent = { logger.info('Locally provided ProjectId: ' + config.projectId); } + if (onUncaughtExceptionValues.indexOf(config.onUncaughtException) === -1) { + logger.error('The value of onUncaughtException should be one of ', + onUncaughtExceptionValues); + throw new Error('Invalid value for onUncaughtException configuration.'); + } + var headers = {}; headers[constants.TRACE_AGENT_REQUEST_HEADER] = 1; diff --git a/lib/trace-agent.js b/lib/trace-agent.js index 20121c7c7..cd81ea68f 100644 --- a/lib/trace-agent.js +++ b/lib/trace-agent.js @@ -43,6 +43,18 @@ function TraceAgent(config, logger) { this.policy = tracingPolicy.createTracePolicy(config); + if (config.onUncaughtException !== 'disregard') { + this.unhandledException = function() { + traceAgent.traceWriter.flushBuffer_(traceAgent.config_.projectId); + if (config.onUncaughtException === 'flushAndExit') { + setTimeout(function() { + process.exit(1); + }, 2000); + } + }; + process.on('uncaughtException', this.unhandledException); + } + logger.info('trace agent activated'); } @@ -55,6 +67,9 @@ TraceAgent.prototype.stop = function() { this.traceWriter.stop(); this.namespace = null; traceAgent = null; + if (this.config_.onUncaughtException !== 'disregard') { + process.removeListener('uncaughtException', this.unhandledException); + } this.logger.info('trace agent deactivated'); }; diff --git a/test/standalone/test-trace-uncaught-exception.js b/test/standalone/test-trace-uncaught-exception.js new file mode 100644 index 000000000..1797ce6fb --- /dev/null +++ b/test/standalone/test-trace-uncaught-exception.js @@ -0,0 +1,79 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var nock = require('nock'); +var cls = require('../../lib/cls.js'); +var agent = require('../..'); +var request = require('request'); + +nock.disableNetConnect(); + +var uri = 'https://cloudtrace.googleapis.com'; +var path = '/v1/projects/0/traces'; + +process.env.GCLOUD_PROJECT = 0; + +var queueSpans = function(n, privateAgent) { + for (var i = 0; i < n; i++) { + privateAgent.createRootSpanData('name', 1, 0).close(); + } +}; + +var formatBuffer = function(buffer) { + return { + traces: buffer.map(function(e) { return JSON.parse(e); }) + }; +}; + +describe('tracewriter publishing', function() { + + it('should publish on unhandled exception', function(done) { + process.removeAllListeners('uncaughtException'); // Remove mocha handler + var buf; + var scope = nock(uri) + .intercept(path, 'PATCH', function(body) { + var parsedOriginal = formatBuffer(buf); + assert.equal(JSON.stringify(body), JSON.stringify(parsedOriginal)); + return true; + }).reply(200); + process.on('uncaughtException', function() { + setTimeout(function() { + assert.equal(process.listeners('uncaughtException').length, 2); + agent.stop(); + assert.equal(process.listeners('uncaughtException').length, 1); + scope.done(); + done(); + }, 20); + }); + process.nextTick(function() { + var privateAgent = agent.start({ + bufferSize: 1000, + samplingRate: 0, + onUncaughtException: 'flush' + }).private_(); + privateAgent.traceWriter.request_ = request; // Avoid authing + cls.getNamespace().run(function() { + queueSpans(2, privateAgent); + buf = privateAgent.traceWriter.buffer_; + throw new Error(':('); + }); + }); + }); + +});