diff --git a/index.js b/index.js index bffdf2f09..d013ddb3b 100644 --- a/index.js +++ b/index.js @@ -24,11 +24,12 @@ require('continuation-local-storage'); var common = require('@google-cloud/common'); var constants = require('./src/constants.js'); +var TraceLabels = require('./src/trace-labels.js'); var gcpMetadata = require('gcp-metadata'); var semver = require('semver'); var traceUtil = require('./src/util.js'); -var SpanData = require('./src/span-data.js'); var util = require('util'); +var PluginApi = require('./src/trace-plugin-interface.js'); var modulesLoadedBeforeTrace = []; @@ -43,31 +44,23 @@ 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 + * Phantom implementation of the trace api. This allows API users to decouple * the enable/disable logic from the calls to the tracing API. The phantom API * has a lower overhead than isEnabled checks inside the API functions. * @private */ var phantomTraceAgent = { - startSpan: function() { return SpanData.nullSpan; }, - endSpan: function(spanData) { spanData.close(); }, - runInSpan: function(name, labels, fn) { - if (typeof(labels) === 'function') { - fn = labels; - } - fn(function() {}); - }, - runInRootSpan: function(name, labels, fn) { - if (typeof(labels) === 'function') { - fn = labels; - } - fn(function() {}); - }, - setTransactionName: function() {}, - addTransactionLabel: function() {} + enhancedDatabaseReportingEnabled: function() { return false; }, + runInRootSpan: function(opts, fn) { return fn(PluginApi.nullSpan); }, + createChildSpan: function(opts) { return PluginApi.nullSpan; }, + wrap: function(fn) { return fn; }, + wrapEmitter: function(ee) { return ee; }, + constants: constants, + labels: TraceLabels }; /** @private */ +// This can be either a PhantomAgent or PluginAPI object. var agent = phantomTraceAgent; var initConfig = function(projectConfig) { @@ -91,36 +84,36 @@ var initConfig = function(projectConfig) { }; /** - * The singleton public agent. This is the public API of the module. + * The singleton public api. This is the public API of the module. */ var publicAgent = { isActive: function() { return agent !== phantomTraceAgent; }, - startSpan: function(name, labels) { - return agent.startSpan(name, labels); + enhancedDatabaseReportingEnabled: function() { + return agent.enhancedDatabaseReportingEnabled(); }, - endSpan: function(spanData, labels) { - return agent.endSpan(spanData, labels); + runInRootSpan: function(opts, fn) { + return agent.runInRootSpan(opts, fn); }, - runInSpan: function(name, labels, fn) { - return agent.runInSpan(name, labels, fn); + createChildSpan: function(opts) { + return agent.createChildSpan(opts); }, - runInRootSpan: function(name, labels, fn) { - return agent.runInRootSpan(name, labels, fn); + wrap: function(fn) { + return agent.wrap(fn); }, - setTransactionName: function(name) { - return agent.setTransactionName(name); + wrapEmitter: function(ee) { + return agent.wrapEmitter(ee); }, - addTransactionLabel: function(key, value) { - return agent.addTransactionLabel(key, value); - }, + constants: constants, + + labels: TraceLabels, start: function(projectConfig) { var config = initConfig(projectConfig); @@ -191,7 +184,7 @@ var publicAgent = { 'service. Please provide a valid project number as an env. ' + 'variable, or through config.projectId passed to start(). ' + err); if (that.isActive()) { - agent.stop(); + agent.agent_.stop(); agent = phantomTraceAgent; } return; @@ -204,7 +197,7 @@ var publicAgent = { return this; } - agent = require('./src/trace-agent.js').get(config, logger); + agent = new PluginApi(require('./src/trace-agent.js').get(config, logger)); return this; // for chaining }, @@ -219,7 +212,7 @@ var publicAgent = { * For use in tests only. * @private */ - private_: function() { return agent; } + private_: function() { return agent.agent_; } }; /** diff --git a/src/span-data.js b/src/span-data.js index c1361f16c..c93fce011 100644 --- a/src/span-data.js +++ b/src/span-data.js @@ -156,13 +156,6 @@ function StackFrame(className, methodName, fileName, lineNumber, columnNumber) { } } -SpanData.nullSpan = { - createChildSpanData: function() { return SpanData.nullSpan; }, - addLabel: function() {}, - addLabels: function() {}, - close: function() {} -}; - /** * Export SpanData. */ diff --git a/src/trace-agent.js b/src/trace-agent.js index 12c0ad95d..484b6fd9f 100644 --- a/src/trace-agent.js +++ b/src/trace-agent.js @@ -126,94 +126,6 @@ TraceAgent.prototype.endSpan = function(spanData, labels) { spanData.close(); }; -/** - * Run the provided function in a new span with the provided name. - * If the provided function accepts a parameter, it is assumed to be - * async and is given a continuation to terminate the span after its - * work is completed. - * @param {string} name The name of the resulting span. - * @param {Object=} labels Labels to be attached - * to the resulting span. Non-object data types are silently ignored. - * @param {function(function()=)} fn The function to trace. - */ -TraceAgent.prototype.runInSpan = function(name, labels, fn) { - if (typeof(labels) === 'function') { - fn = labels; - labels = undefined; - } - var span = this.startSpan(name, labels, 1); - if (fn.length === 0) { - fn(); - this.endSpan(span); - } else { - fn(this.endSpan.bind(this, span)); - } -}; - -/** - * Run the provided function in a new root span with the provided - * name. As with runInSpan, if the provided function accepts a parameter, - * it is assumed to be asynchronous, and a callback will be passed - * to the function to terminate the root span. - * @param {string} name The name of the resulting root span. - * @param {Object=} labels Labels to be attached - * to the resulting span. - * @param {function(function()=)} fn The function to trace. - */ -TraceAgent.prototype.runInRootSpan = function(name, labels, fn) { - if (typeof(labels) === 'function') { - fn = labels; - labels = undefined; - } - var self = this; - this.namespace.run(function () { - if (cls.getRootContext()) { - self.logger.error('Can\'t nest root spans'); - return; - } - var span = self.createRootSpanData(name, null, null, 3, 'SPAN_KIND_UNSPECIFIED'); - span.addLabels(labels); - if (fn.length === 0) { - fn(); - span.close(); - } else { - fn(function () { - span.close(); - }); - } - }); -}; - -/** - * Set the name of the root transaction. - * @param {string} name The new name for the current root transaction. - */ -TraceAgent.prototype.setTransactionName = function(name) { - var rootSpan = cls.getRootContext(); - if (rootSpan === SpanData.nullSpan) { - return; - } - if (rootSpan) { - rootSpan.span.name = name; - } else { - this.logger.error('Cannot set transaction name without an active transaction'); - } -}; - -/** - * Add a new key value label to the root transaction. - * @param {string} key The key for the new label. - * @param {string} value The value for the new label. - */ -TraceAgent.prototype.addTransactionLabel = function(key, value) { - var rootSpan = cls.getRootContext(); - if (rootSpan) { - rootSpan.addLabel(key, value); - } else { - this.logger.error('Cannot add label without an active transaction'); - } -}; - /** * Determines whether a trace of the given name should be recorded based * on the current tracing policy. diff --git a/src/trace-labels.js b/src/trace-labels.js index 787736d31..bbb4eceda 100644 --- a/src/trace-labels.js +++ b/src/trace-labels.js @@ -19,8 +19,7 @@ /** * Well-known trace span label values. */ -function TraceLabels() { -} +var TraceLabels = {}; /** diff --git a/src/trace-plugin-interface.js b/src/trace-plugin-interface.js index bb0682059..8dfc708b5 100644 --- a/src/trace-plugin-interface.js +++ b/src/trace-plugin-interface.js @@ -100,6 +100,12 @@ RootSpan.prototype.getTraceContext = function() { return this.serializedTraceContext_; }; +var nullSpan = { + addLabel: function(k, v) {}, + endSpan: function() {}, + getTraceContext: function() { return ''; } +}; + /** * PluginAPI constructor. Don't call directly - a plugin object will be passed to * plugin themselves @@ -130,43 +136,6 @@ PluginAPI.prototype.summarizeDatabaseResults = function(res) { this.agent_.config_.databaseResultReportingSize); }; -/** - * Creates and returns a new RootSpan object corresponding to an incoming - * request. - * @param {object} options An object that specifies options for how the root - * span is created and propogated. - * @param {string} options.name The name to apply to the root span. - * @param {?string} options.url A URL associated with the root span, if - * applicable. - * @param {?string} options.traceContext The serialized form of an object that - * contains information about an existing trace context. - * @param {?number} options.skipFrames The number of stack frames to skip when - * collecting call stack information for the root span, starting from the top; - * this should be set to avoid including frames in the plugin. Defaults to 0. - * @returns A new RootSpan object, or null if the trace agent's policy has - * disabled tracing for the given set of options. - */ -PluginAPI.prototype.createRootSpan = function(options) { - var skipFrames = options.skipFrames ? options.skipFrames + 1 : 1; - return createRootSpan_(this, options, skipFrames); -}; - -/** - * Returns a RootSpan object that corresponds to a root span started earlier - * in the same context, or null if one doesn't exist. - * @returns A new RootSpan object, or null if a root span doesn't exist in - * the current context. - */ -PluginAPI.prototype.getRootSpan = function() { - if (cls.getRootContext()) { - return new RootSpan(this.agent_, cls.getRootContext()); - } else { - this.logger_.warn('Attempted to get root span when it doesn\'t' + - ' exist'); - return null; - } -}; - /** * Runs the given function in a root span corresponding to an incoming request, * possibly passing it an object that exposes an interface for adding labels @@ -187,6 +156,10 @@ PluginAPI.prototype.runInRootSpan = function(options, fn) { 'root span.'); return fn(null); } + if (cls.getRootContext()) { + this.logger_.warn('Trace agent: Cannot create nested root spans.'); + return fn(null); + } return this.agent_.namespace.runAndReturn(function() { var skipFrames = options.skipFrames ? options.skipFrames + 2 : 2; var rootSpan = createRootSpan_(that, options, skipFrames); @@ -195,15 +168,14 @@ PluginAPI.prototype.runInRootSpan = function(options, fn) { }; /** - * Creates and returns a new ChildSpan object nested within the root span object - * returned by getRootSpan. If there is no current RootSpan object, this - * function returns null. + * Creates and returns a new ChildSpan object nested within the root span. If + * there is no current RootSpan object, this function returns null. * @param {object} options An object that specifies options for how the child * span is created and propogated. * @returns A new ChildSpan object, or null if there is no active root span. */ PluginAPI.prototype.createChildSpan = function(options) { - var rootSpan = this.getRootSpan(); + var rootSpan = getRootSpan_(this); if (rootSpan) { options = options || {}; var childContext = this.agent_.startSpan(options.name, {}, @@ -247,6 +219,8 @@ PluginAPI.prototype.constants = constants; PluginAPI.prototype.labels = TraceLabels; +PluginAPI.nullSpan = nullSpan; + module.exports = PluginAPI; // Module-private functions @@ -270,3 +244,13 @@ function createRootSpan_(api, options, skipFrames) { skipFrames + 1); return new RootSpan(api.agent_, rootContext); } + +function getRootSpan_(api) { + if (cls.getRootContext()) { + return new RootSpan(api.agent_, cls.getRootContext()); + } else { + api.logger_.warn('Attempted to get root span when it doesn\'t' + + ' exist'); + return null; + } +} diff --git a/test/hooks/common.js b/test/hooks/common.js index 86901ed10..b533d659e 100644 --- a/test/hooks/common.js +++ b/test/hooks/common.js @@ -47,11 +47,18 @@ function init(agent) { }; } +function replaceFunction(target, prop, fn) { + var old = target[prop]; + target[prop] = fn; + return old; +} + function replaceDebugLogger(agent, fn) { - var privateAgent = agent.private_(); - var oldDebug = privateAgent.logger.debug; - privateAgent.logger.debug = fn; - return oldDebug; + return replaceFunction(agent.private_().logger, 'debug', fn); +} + +function replaceTracingPolicy(agent, fn) { + return replaceFunction(agent.private_(), 'policy', fn); } /** @@ -248,6 +255,7 @@ module.exports = { runInTransaction: runInTransaction, getShouldTraceArgs: getShouldTraceArgs, replaceDebugLogger: replaceDebugLogger, + replaceTracingPolicy: replaceTracingPolicy, createRootSpanData: createRootSpanData, clearNamespace: clearNamespace, getConfig: getConfig, diff --git a/test/test-agent-metadata.js b/test/test-agent-metadata.js index bc12022ff..428fd1429 100644 --- a/test/test-agent-metadata.js +++ b/test/test-agent-metadata.js @@ -59,28 +59,33 @@ describe('agent interaction with metadata service', function() { }); it('should preserve public interface when stopped', function(done) { - nock.disableNetConnect(); - var scope = nock('http://metadata.google.internal') - .get('/computeMetadata/v1/project/project-id') - .times(2) - .reply(404, 'foo'); assert.equal(typeof agent, 'object'); - assert.equal(typeof agent.startSpan, 'function'); - assert.equal(typeof agent.endSpan, 'function'); - assert.equal(typeof agent.runInSpan, 'function'); + assert.equal(typeof agent.isActive, 'function'); + assert.equal(typeof agent.enhancedDatabaseReportingEnabled, 'function'); assert.equal(typeof agent.runInRootSpan, 'function'); - assert.equal(typeof agent.setTransactionName, 'function'); - assert.equal(typeof agent.addTransactionLabel, 'function'); - agent = trace.start({logLevel: 0}); + assert.equal(typeof agent.createChildSpan, 'function'); + assert.equal(typeof agent.wrap, 'function'); + assert.equal(typeof agent.wrapEmitter, 'function'); + assert.equal(typeof agent.constants, 'object'); + assert.equal(typeof agent.labels, 'object'); + agent = trace.start({logLevel: 0, enabled: false}); setTimeout(function() { assert.equal(typeof agent, 'object'); - assert.equal(typeof agent.startSpan, 'function'); - assert.equal(typeof agent.endSpan, 'function'); - assert.equal(typeof agent.runInSpan, 'function'); - assert.equal(typeof agent.runInRootSpan, 'function'); - assert.equal(typeof agent.setTransactionName, 'function'); - assert.equal(typeof agent.addTransactionLabel, 'function'); - scope.done(); + assert.equal(agent.isActive(), false); + assert.equal(agent.enhancedDatabaseReportingEnabled(), false); + agent.runInRootSpan({}, function(root) { + assert.equal(typeof root.addLabel, 'function'); + assert.equal(typeof root.endSpan, 'function'); + assert.equal(root.getTraceContext(), ''); + }); + var child = agent.createChildSpan({}); + assert.equal(typeof child.addLabel, 'function'); + assert.equal(typeof child.endSpan, 'function'); + assert.equal(child.getTraceContext(), ''); + assert.strictEqual(agent.wrap(agent), agent); + assert.strictEqual(agent.wrapEmitter(agent), agent); + assert.equal(typeof agent.constants, 'object'); + assert.equal(typeof agent.labels, 'object'); done(); }, 500); }); diff --git a/test/test-index.js b/test/test-index.js index 6e1ce7fba..6cb4fb9a8 100644 --- a/test/test-index.js +++ b/test/test-index.js @@ -23,15 +23,15 @@ if (!process.env.GCLOUD_PROJECT) { var assert = require('assert'); var trace = require('..'); -var cls = require('../src/cls.js'); var common = require('./hooks/common.js'); -var TraceLabels = require('../src/trace-labels.js'); +var TracingPolicy = require('../src/tracing-policy.js'); describe('index.js', function() { var agent = trace.start(); afterEach(function() { - common.getTraceWriter(agent).buffer_ = []; + common.cleanTraces(agent); + common.clearNamespace(agent); }); it('should get the agent with `Trace.get`', function() { @@ -51,170 +51,80 @@ describe('index.js', function() { describe('labels', function(){ it('should add labels to spans', function() { - cls.getNamespace().run(function() { - common.createRootSpanData(agent, 'root', 1, 2); - var spanData = agent.startSpan('sub', {test1: 'value'}); - agent.endSpan(spanData); - var traceSpan = spanData.span; + agent.runInRootSpan({name: 'root', url: 'root'}, function(root) { + var child = agent.createChildSpan({name: 'sub'}); + child.addLabel('test1', 'value'); + child.endSpan(); + var traceSpan = child.span_.span; assert.equal(traceSpan.name, 'sub'); assert.ok(traceSpan.labels); assert.equal(traceSpan.labels.test1, 'value'); + root.endSpan(); }); }); - - it('should ignore non-object labels', function() { - cls.getNamespace().run(function() { - common.createRootSpanData(agent, 'root', 1, 2); - - var testLabels = [ - 'foo', - 5, - undefined, - null, - true, - false, - [4,5,6], - function () {} - ]; - - testLabels.forEach(function(labels) { - var spanData = agent.startSpan('sub', labels); - agent.endSpan(spanData); - var spanLabels = spanData.span.labels; - // Only the default labels should be there. - var keys = Object.keys(spanLabels); - assert.equal(keys.length, 1, 'should have only 1 key'); - assert.equal(keys[0], TraceLabels.STACK_TRACE_DETAILS_KEY); - }); - }); - }); - }); - - it('should produce real spans runInSpan sync', function() { - cls.getNamespace().run(function() { - var root = common.createRootSpanData(agent, 'root', 1, 0); - var testLabel = { key: 'val' }; - agent.runInSpan('sub', testLabel, function() {}); - root.close(); - var spanPredicate = function(spanData) { - return spanData.spans[1].name === 'sub'; - }; - var matchingSpans = common.getTraceWriter(agent).buffer_ - .map(JSON.parse) - .filter(spanPredicate); - assert.equal(matchingSpans.length, 1); - assert.equal(matchingSpans[0].spans[1].labels.key, 'val'); - }); - }); - - it('should produce real spans runInSpan async', function(done) { - cls.getNamespace().run(function() { - var root = common.createRootSpanData(agent, 'root', 1, 0); - var testLabel = { key: 'val' }; - agent.runInSpan('sub', function(endSpan) { - setTimeout(function() { - endSpan(testLabel); - root.close(); - var spanPredicate = function(spanData) { - return spanData.spans[1].name === 'sub'; - }; - var matchingSpans = common.getTraceWriter(agent).buffer_ - .map(JSON.parse) - .filter(spanPredicate); - assert.equal(matchingSpans.length, 1); - var span = matchingSpans[0].spans[1]; - var duration = Date.parse(span.endTime) - Date.parse(span.startTime); - assert(duration > 190); - assert(duration < 300); - assert.equal(span.labels.key, 'val'); - // mocha seems to schedule the next test in the same context in 0.12. - cls.setRootContext(null); - done(); - }, 200); - }); - }); - }); - - it('should produce real root spans runInRootSpan sync', function() { - cls.getNamespace().run(function() { - var testLabel = { key: 'val' }; - agent.runInRootSpan('root', testLabel, function() { - var childSpan = agent.startSpan('sub'); - agent.endSpan(childSpan); - }); - var spanPredicate = function(spanData) { - return spanData.spans[0].name === 'root' && spanData.spans[1].name === 'sub'; - }; - var matchingSpans = common.getTraceWriter(agent).buffer_ - .map(JSON.parse) - .filter(spanPredicate); - assert.equal(matchingSpans.length, 1); - assert.equal(matchingSpans[0].spans[0].labels.key, 'val'); + it('should produce real child spans', function(done) { + agent.runInRootSpan({name: 'root'}, function(root) { + var child = agent.createChildSpan({name: 'sub'}); + setTimeout(function() { + child.addLabel('key', 'val'); + child.endSpan(); + root.endSpan(); + var spanPredicate = function(span) { + return span.name === 'sub'; + }; + var matchingSpan = common.getMatchingSpan(agent, spanPredicate); + var duration = Date.parse(matchingSpan.endTime) - Date.parse(matchingSpan.startTime); + assert(duration > 190); + assert(duration < 300); + assert.equal(matchingSpan.labels.key, 'val'); + done(); + }, 200); }); }); it('should produce real root spans runInRootSpan async', function(done) { - cls.getNamespace().run(function() { - var testLabel = { key: 'val' }; - agent.runInRootSpan('root', testLabel, function(endSpan) { - var childSpan = agent.startSpan('sub'); - setTimeout(function() { - agent.endSpan(childSpan); - endSpan(testLabel); - var spanPredicate = function(spanData) { - return spanData.spans[0].name === 'root' && spanData.spans[1].name === 'sub'; - }; - var matchingSpans = common.getTraceWriter(agent).buffer_ - .map(JSON.parse) - .filter(spanPredicate); - assert.equal(matchingSpans.length, 1); - var span = matchingSpans[0].spans[0]; - var duration = Date.parse(span.endTime) - Date.parse(span.startTime); - assert(duration > 190); - assert(duration < 300); - assert.equal(span.labels.key, 'val'); - // mocha seems to schedule the next test in the same context in 0.12. - cls.setRootContext(null); - done(); - }, 200); - }); + agent.runInRootSpan({name: 'root', url: 'root'}, function(rootSpan) { + rootSpan.addLabel('key', 'val'); + var childSpan = agent.createChildSpan({name: 'sub'}); + setTimeout(function() { + childSpan.endSpan(); + rootSpan.endSpan(); + var spanPredicate = function(span) { + return span.name === 'root'; + }; + var matchingSpan = common.getMatchingSpan(agent, spanPredicate); + var duration = Date.parse(matchingSpan.endTime) - Date.parse(matchingSpan.startTime); + assert(duration > 190); + assert(duration < 300); + assert.equal(matchingSpan.labels.key, 'val'); + done(); + }, 200); }); }); - it('should not break with no root span', function() { - var span = agent.startSpan(); - agent.setTransactionName('noop'); - agent.addTransactionLabel('noop', 'noop'); - agent.endSpan(span); - }); - it('should not allow nested root spans', function(done) { - agent.runInRootSpan('root', function(cb1) { + agent.runInRootSpan({name: 'root', url: 'root'}, function(rootSpan1) { var finished = false; var finish = function () { assert(!finished); finished = true; - cb1(); - var spanPredicate = function(spanData) { - return spanData.spans[0].name === 'root'; + rootSpan1.endSpan(); + var spanPredicate = function(span) { + return span.name === 'root'; }; - var matchingSpans = common.getTraceWriter(agent).buffer_ - .map(JSON.parse) - .filter(spanPredicate); - assert.equal(matchingSpans.length, 1); - var span = matchingSpans[0].spans[0]; - var duration = Date.parse(span.endTime) - Date.parse(span.startTime); + var matchingSpan = common.getMatchingSpan(agent, spanPredicate); + var duration = Date.parse(matchingSpan.endTime) - Date.parse(matchingSpan.startTime); assert(duration > 190); assert(duration < 300); done(); }; setTimeout(function() { - agent.runInRootSpan('root2', function(cb2) { + agent.runInRootSpan({name: 'root2', url: 'root2'}, function(rootSpan2) { setTimeout(function() { // We shouldn't reach this point - cb2(); + rootSpan2.endSpan(); finish(); }, 200); }); @@ -223,13 +133,12 @@ describe('index.js', function() { }); }); - it('should set transaction name and labels', function() { - cls.getNamespace().run(function() { - var spanData = common.createRootSpanData(agent, 'root', 1, 2); - agent.setTransactionName('root2'); - agent.addTransactionLabel('key', 'value'); - assert.equal(spanData.span.name, 'root2'); - assert.equal(spanData.span.labels.key, 'value'); + it('should respect sampling policy', function(done) { + var oldPolicy = common.replaceTracingPolicy(agent, new TracingPolicy.TraceNonePolicy()); + agent.runInRootSpan({name: 'root', url: 'root'}, function(rootSpan) { + assert.strictEqual(rootSpan, null); + common.replaceTracingPolicy(agent, oldPolicy); + done(); }); });