blob: c9c82275fa8688aa760b0a56cdc723d5bb6cbf26 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var uniqueid = require('../lib/uniqueid');
var context = require('../context');
var vdl = require('../gen-vdl/v.io/v23/vtrace');
var vlog = require('../lib/vlog');
var spanKey = context.ContextKey();
var storeKey = context.ContextKey();
var second = 1000;
var minute = 60 * second;
var hour = 60 * minute;
var indentStep = ' ';
module.exports = {
withNewTrace: withNewTrace,
withContinuedTrace: withContinuedTrace,
withNewSpan: withNewSpan,
withNewStore: withNewStore,
getSpan: getSpan,
getStore: getStore,
forceCollect: forceCollect,
formatTraces: formatTraces,
request: request,
response: response
};
/**
* Create a map key from a uniqueid.Id.
* @private
* @param {Object} A uniqueid.Id instance.
* @return {string} A string key for use as a map key.
*/
function key(id) {
return uniqueid.toHexString(id);
}
/**
* @summary A Span represents a named span of time.
* @description
* <p>Private constructor, use {@link module:vanadium.vtrace.getSpan}.</p>
* Spans have a beginning and can contain annotations which mark
* specific moments.
* @constructor
* @param {string} name The name of the Span.
* @param {Object} store A vtrace Store instance.
* @param {Object} trace A uniqueid.Id instance identifying the trace.
* @param {Object} parent A uniqueid.Id instance identifying this Spans parent.
* @memberof module:vanadium.vtrace
* @inner
*/
function Span(name, store, trace, parent) {
if (!(this instanceof Span)) {
return new Span(name, trace, parent, store);
}
Object.defineProperty(this, 'name', {
writable: false,
value: name
});
Object.defineProperty(this, 'id', {
writable: false,
value: uniqueid.random()
});
if (trace === undefined && parent === undefined) {
parent = uniqueid.random();
trace = parent;
}
Object.defineProperty(this, 'parent', {
writable: false,
value: parent
});
Object.defineProperty(this, 'trace', {
writable: false,
value: trace
});
this._store = store;
store._start(this);
}
/**
* Adds an annotation to the Span.
* @param {string} msg A string annotation.
*/
Span.prototype.annotate = function(msg) {
this._store._annotate(this, msg);
};
/**
* Marks the current Span as finished, recording the end time.
*/
Span.prototype.finish = function() {
this._store._finish(this);
};
function Node(id) {
this.id = id;
this.spans = {};
}
Node.prototype.record = function() {
var record = new vdl.TraceRecord();
record.id = this.id;
for (var id in this.spans) {
if (!this.spans.hasOwnProperty(id)) {
continue;
}
var span = this.spans[id];
record.spans.push(new vdl.SpanRecord(span));
}
return record;
};
/**
* @summary Store collects the information of interesting traces in memory.
* @description
* Private constructor. Use {@link module:vanadium.vtrace.getStore} <br>
* A vtrace Store.
* A Store is responsible for saving traces for later reporting and analysis.
* @constructor
* @inner
* @memberof module:vanadium.vtrace
*/
function Store() {
if (!(this instanceof Store)) {
return new Store();
}
this._collectRegexp = null;
this._nodes = {};
}
/**
* Filters the information collected by the Store
* @param {string} regexp The regular expression that must
* be matched by the span name in order for that span to be
* collected
*/
Store.prototype.setCollectRegexp = function(regexp) {
this._collectRegexp = new RegExp(regexp);
};
Store.prototype._flags = function(id) {
var node = this._nodes[key(id)];
if (!node) {
return vdl.Empty;
}
return vdl.CollectInMemory;
};
/**
* Returns vtrace.TraceRecord instances for all traces recorded by the store.
* @return {Array<module:vanadium.vtrace.TraceRecord>} An array of
* vtrace.TraceRecord instances.
*/
Store.prototype.traceRecords = function() {
var out = [];
for (var key in this._nodes) {
if (!this._nodes.hasOwnProperty(key)) {
continue;
}
out.push(this._nodes[key].record());
}
return out;
};
/**
* Returns a [TraceRecord]{@link module:vanadium.vtrace.TraceRecord} for
* the given trace id.
* @param {module:vanadium.uniqueId.Id} id A uniqueid.Id instance.
* @return {module:vanadium.vtrace.TraceRecord} a vtrace.TraceRecord instance.
*/
Store.prototype.traceRecord = function(id) {
var node = this._nodes[key(id)];
if (!node) {
var record = vdl.TraceRecord();
record.id = id;
return record;
}
return node.record();
};
// _getNode get's a trace node from the store. shouldCreate
// is either a boolean or a function that returns a boolean.
// if shouldCreate or shouldCreate() is true, then we will create the node
// if it does not exist, otherwise we'll return null.
Store.prototype._getNode = function(traceid, shouldCreate) {
var k = key(traceid);
var node = this._nodes[k];
if (node) {
return node;
}
if (typeof shouldCreate === 'function') {
shouldCreate = shouldCreate();
}
if (shouldCreate) {
node = new Node(traceid);
this._nodes[k] = node;
}
return node;
};
Store.prototype._getSpan = function(span, shouldCreate) {
var node = this._getNode(span.trace, shouldCreate);
if (!node) {
return null;
}
var spankey = key(span.id);
var record = node.spans[spankey];
if (!record) {
record = new vdl.SpanRecord();
record.id = span.id;
record.parent = span.parent;
record.name = span.name;
node.spans[spankey] = record;
}
return record;
};
Store.prototype._start = function(span) {
var store = this;
var record = this._getSpan(span, function() {
var re = store._collectRegexp;
return re && re.test(span.name);
});
if (record) {
record.start = store._now();
}
};
Store.prototype._finish = function(span) {
var store = this;
var record = this._getSpan(span, function() {
var re = store._collectRegexp;
return re && re.test(span.name);
});
if (record) {
record.end = store._now();
}
};
Store.prototype._annotate = function(span, msg) {
var store = this;
var record = this._getSpan(span, function() {
var re = store._collectRegexp;
return re && re.test(msg);
});
if (record) {
var annotation = new vdl.Annotation();
annotation.when = store._now();
annotation.message = msg;
record.annotations.push(annotation);
}
};
Store.prototype._now = function() {
return new Date();
};
/**
* Merges a response into the store, adding information on the
* Span in contains into the local database.
* @param {module:vanadium.vtrace~Response} response A
* [Response]{@link module.vanadium.vtrace~Response} instance.
*/
Store.prototype.merge = function(response) {
if (!uniqueid.valid(response.trace.id)) {
return;
}
var shouldCreate = (response.flags & vdl.CollectInMemory) !== 0;
var node = this._getNode(response.trace.id, shouldCreate);
if (!node) {
return;
}
var spans = response.trace.spans;
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
node.spans[key(span.id)] = span;
}
};
/**
* Creates a new [Span]{@link module:vanadium.vtrace~Span} that represents
* the beginning of a new trace and attaches it to a new context derived from
* ctx. This should be used when starting operations unrelated to other
* ongoing traces.
* @param {module:vanadium.context.Context} ctx A context.Context instance
* to derive a new context from.
* @return {module:vanadium.context.Context} A new context with a new Span
* attached.
* @memberof module:vanadium.vtrace
*/
function withNewTrace(ctx) {
return ctx.withValue(spanKey, new Span('', ctx.value(storeKey)));
}
/**
* Creates a new [Span]{@link module:vanadium.vtrace~Span} that continues
* a trace represented in request. The new Span will be attached to the
* returned context.
* @param {module:vanadium.context.Context} ctx A context.Context instance to
* derive a new context from.
* @param {string} name The name of the new Span.
* @param {module:vanadium.vtrace~Request} request A
* [Request]{@link module:vanadium.vtrace~Request} instance.
* @return {module:vanadium.context.Context} A new context with a new Span
* attached.
* @memberof module:vanadium.vtrace
*/
function withContinuedTrace(ctx, name, request) {
var store = ctx.value(storeKey);
if (request.flags & vdl.CollectInMemory !== 0) {
store._getNode(request.traceId, true);
}
var span = new Span(name, store, request.traceId, request.spanId);
return ctx.withValue(spanKey, span);
}
/**
* Creates a new [Span]{@link module:vanadium.vtrace~Span} that continues
* the trace attached to ctx.
* @param {module:vanadium.context.Context} ctx A context.Context instance to
* derive a new context from.
* @param {string} name The name of the new Span.
* @return {module:vanadium.context.Context} A new context with a new Span
* attached.
* @memberof module:vanadium.vtrace
*/
function withNewSpan(ctx, name) {
var oldSpan = ctx.value(spanKey);
var oldStore = ctx.value(storeKey);
var span = new Span(name, oldStore, oldSpan.trace, oldSpan.id);
return ctx.withValue(spanKey, span);
}
/**
* Return the [Span]{@link module:vanadium.vtrace~Span} attached to ctx.
* @param {module:vanadium.context.Context} ctx A context.Context instance.
* @return {module:vanadium.vtrace.SpanRecord} A Span instance.
* @memberof module:vanadium.vtrace
*/
function getSpan(ctx) {
return ctx.value(spanKey);
}
/**
* Creates a new [Store]{@link module:vanadium.vtrace~Store} and returns
* a new context derived from ctx with the store attached.
* @param {module:vanadium.context.Context} ctx A context.Context instance to
* derive a new context from.
* @return {module:vanadium.context.Context} A new context with a new Store
* attached.
* @memberof module:vanadium.vtrace
*/
function withNewStore(ctx) {
var store = new Store();
return ctx.withValue(storeKey, store);
}
/**
* Return the Store attached to ctx.
* @param {module:vanadium.context.Context} ctx A context.Context instance.
* @return {module:vanadium.vtrace~Store} A {@link Store} instance.
* @memberof module:vanadium.vtrace
*/
function getStore(ctx) {
return ctx.value(storeKey);
}
/**
* Force collection of the current trace.
* @param {module:vanadium.context.Context} ctx A context.Context instance.
* @memberof module:vanadium.vtrace
*/
function forceCollect(ctx) {
var store = ctx.value(storeKey);
var span = ctx.value(spanKey);
store._getNode(span.trace, true);
}
/**
* Generate a [Request]{@link module:vanadium.vtrace~Request} to send over
* the wire.
* @param {module:vanadium.context.Context} ctx A context.Context instance.
* @return {module:vanadium.vtrace~Request} request a
* [Request]{@link module:vanadium.vtrace~Request} instance.
* @memberof module:vanadium.vtrace
*/
function request(ctx) {
var store = ctx.value(storeKey);
var span = ctx.value(spanKey);
return vdl.Request({
spanId: span.id,
traceId: span.trace,
flags: store._flags(span.trace)
});
}
/**
* Generate a [Response]{@link module:vanadium.vtrace~Response} to send over the
* wire.
* @param {module:vanadium.context.Context} ctx A context.Context instance.
* @return {module:vanadium.vtrace~Response} A
* [Response]{@link module:vanadium.vtrace~Response} instance.
* @memberof module:vanadium.vtrace
*/
function response(ctx) {
if (!ctx) {
vlog.logger.warn('Cannot perform vtrace without valid context');
return;
}
var store = ctx.value(storeKey);
var span = ctx.value(spanKey);
return vdl.Response({
flags: store._flags(span.trace),
trace: store.traceRecord(span.trace)
});
}
var zeroMs = Date.parse('0001-01-01');
// Returns true if the given date is the zero date, by the definition of VDL.
function isZeroDate(d) {
return d.getTime() === zeroMs;
}
function Tree(span) {
this._span = span;
this._children = [];
}
function buildTree(record) {
var t;
var tid;
var root;
var earliest = new Date(zeroMs);
var traceKey = key(record.id);
var trees = {};
record.spans.forEach(function(span) {
// We want to find the lowest valid (non-zero) timestamp in the trace.
// If we have a non-zero timestamp, save it if it's the earliest (or
// this is the first valid timestamp we've seen).
if (!isZeroDate(span.start)) {
if (isZeroDate(earliest) || span.start < earliest) {
earliest = span.start;
}
}
tid = key(span.id);
t = trees[tid];
if (!t) {
t = new Tree(span);
trees[tid] = t;
}
var parentKey = key(span.parent);
if (parentKey === traceKey) {
root = t;
} else {
var parent = trees[parentKey];
if (!parent) {
parent = new Tree();
trees[parentKey] = parent;
}
parent._children.push(t);
}
});
// Sort the children of each node in start time order, and the
// annotations in time order.
for (tid in trees) {
if (!trees.hasOwnProperty(tid)) {
continue;
}
t = trees[tid];
t._children.sort(function(a, b) {
return a.start - b.start;
});
if (t._span && t._span.annotations) {
t._span.annotations.sort(function(a, b) {
return a.when - b.when;
});
}
}
// If we didn't find the root of the trace, create a stand-in.
if (!root) {
root = new Tree(new vdl.SpanRecord({
name: 'Missing Root Span',
start: earliest
}));
} else if (isZeroDate(root._span.start)) {
root._span.start = earliest;
}
// Find all nodes that have no span. These represent missing data
// in the tree. We invent fake "missing" spans to represent
// (perhaps several) layers of missing spans. Then we add these as
// children of the root.
var missing = [];
for (tid in trees) {
if (!trees.hasOwnProperty(tid)) {
continue;
}
t = trees[tid];
if (!t._span) {
t._span = new vdl.SpanRecord({
name: 'Missing Data'
});
missing.push(t);
}
}
root._children = root._children.concat(missing);
root._children.sort(function(a, b) {
return a.start - b.start;
});
return root;
}
function formatDelta(when, start) {
if (isZeroDate(when)) {
return '??';
}
var out = '';
var delta = when - start;
if (delta === 0) {
return '0';
}
if (delta < 0) {
out += '-';
delta = -delta;
}
if (delta < second) {
return delta + 'ms';
}
if (delta > hour) {
var hours = Math.floor(delta / hour);
delta -= hours * hour;
out += hours + 'h';
}
if (delta > minute) {
var minutes = Math.floor(delta / minute);
delta -= minutes * minute;
out += minutes + 'm';
}
out += (delta / 1000) + 's';
return out;
}
function formatTime(when) {
if (isZeroDate(when)) {
return '??';
}
return when.toISOString();
}
function formatAnnotations(annotations, traceStart, indent) {
var out = '';
for (var a = 0; a < annotations.length; a++) {
var annotation = annotations[a];
out += indent + '@' + formatDelta(annotation.when, traceStart);
out += ' ' + annotation.message + '\n';
}
return out;
}
function formatTree(tree, traceStart, indent) {
var span = tree._span;
var out = indent + 'Span - ' + span.name;
out += ' [id: ' + key(span.id).slice(24);
out += ' parent: ' + key(span.parent).slice(24) + ']';
out += ' (' + formatDelta(span.start, traceStart);
out += ', ' + formatDelta(span.end, traceStart) + ')\n';
indent += indentStep;
out += formatAnnotations(span.annotations, traceStart, indent);
for (var c = 0; c < tree._children.length; c++) {
out += formatTree(tree._children[c], traceStart, indent);
}
return out;
}
function formatTrace(record) {
var root = buildTree(record);
if (!root) {
return null;
}
var span = root._span;
var out = 'Trace - ' + key(record.id);
out += ' (' + formatTime(span.start) + ', ' + formatTime(span.end) + ')\n';
out += formatAnnotations(span.annotations, span.start, indentStep);
for (var c = 0; c < root._children.length; c++) {
out += formatTree(root._children[c], span.start, indentStep);
}
return out;
}
/**
* Return a string representation of a trace (or array of traces).
* @param {Array<module:vanadium.vtrace.TraceRecord>} traces Trace records.
* @return {string} A human friendly string representation of the trace.
* @memberof module:vanadium.vtrace
*/
function formatTraces(traces) {
if (!Array.isArray(traces)) {
traces = [traces];
}
if (traces.length === 0) {
return '';
}
var out = 'Vtrace traces:\n';
for (var r = 0; r < traces.length; r++) {
out += formatTrace(traces[r]);
}
return out;
}