packages/core/lib/segments/attributes/subsegment.js (258 lines of code) (raw):

var crypto = require('crypto'); var CapturedException = require('./captured_exception'); var RemoteRequestData = require('./remote_request_data'); var SegmentEmitter = require('../../segment_emitter'); var SegmentUtils = require('../segment_utils'); var Utils = require('../../utils'); var logger = require('../../logger'); /** * Represents a subsegment. * @constructor * @param {string} name - The name of the subsegment. */ function Subsegment(name) { this.init(name); } Subsegment.prototype.init = function init(name) { if (typeof name != 'string') { throw new Error('Subsegment name must be of type string.'); } this.id = crypto.randomBytes(8).toString('hex'); this.name = name; this.start_time = SegmentUtils.getCurrentTime(); this.in_progress = true; this.counter = 0; this.notTraced = false; }; /** * Nests a new subsegment to the array of subsegments. * @param {string} name - The name of the new subsegment to append. * @returns {Subsegment} - The newly created subsegment. */ Subsegment.prototype.addNewSubsegment = function addNewSubsegment(name) { const subsegment = new Subsegment(name); this.addSubsegment(subsegment); return subsegment; }; Subsegment.prototype.addSubsegmentWithoutSampling = function addSubsegmentWithoutSampling(subsegment) { this.addSubsegment(subsegment); subsegment.notTraced = true; }; Subsegment.prototype.addNewSubsegmentWithoutSampling = function addNewSubsegmentWithoutSampling(name) { const subsegment = new Subsegment(name); this.addSubsegment(subsegment); subsegment.notTraced = true; return subsegment; }; /** * Adds a subsegment to the array of subsegments. * @param {Subsegment} subsegment - The subsegment to append. */ Subsegment.prototype.addSubsegment = function(subsegment) { if (!(subsegment instanceof Subsegment)) { throw new Error('Failed to add subsegment:' + subsegment + ' to subsegment "' + this.name + '". Not a subsegment.'); } if (this.subsegments === undefined) { this.subsegments = []; } subsegment.segment = this.segment; subsegment.parent = this; subsegment.notTraced = subsegment.parent.notTraced; subsegment.noOp = subsegment.parent.noOp; if (subsegment.end_time === undefined) { this.incrementCounter(subsegment.counter); } this.subsegments.push(subsegment); }; /** * Removes the subsegment from the subsegments array, used in subsegment streaming. */ Subsegment.prototype.removeSubsegment = function removeSubsegment(subsegment) { if (!(subsegment instanceof Subsegment)) { throw new Error('Failed to remove subsegment:' + subsegment + ' from subsegment "' + this.name + '". Not a subsegment.'); } if (this.subsegments !== undefined) { var index = this.subsegments.indexOf(subsegment); if (index >= 0) { this.subsegments.splice(index, 1); } } }; /** * Adds a property with associated data into the subsegment. * @param {string} name - The name of the property to add. * @param {Object} data - The data of the property to add. */ Subsegment.prototype.addAttribute = function addAttribute(name, data) { this[name] = data; }; /** * Adds a subsegement id to record ordering. * @param {string} id - A subsegment id. */ Subsegment.prototype.addPrecursorId = function(id) { if (typeof id !== 'string') { logger.getLogger().error('Failed to add id:' + id + ' to subsegment ' + this.name + '. Precursor Ids must be of type string.'); } if (this.precursor_ids === undefined) { this.precursor_ids = []; } this.precursor_ids.push(id); }; /** * Adds a key-value pair that can be queryable through GetTraceSummaries. * Only acceptable types are string, float/int and boolean. * @param {string} key - The name of key to add. * @param {boolean|string|number} value - The value to add for the given key. */ Subsegment.prototype.addAnnotation = function(key, value) { if (typeof value !== 'boolean' && typeof value !== 'string' && !isFinite(value)) { logger.getLogger().error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + this.name + '. Value must be of type string, number or boolean.'); return; } if (typeof key !== 'string') { logger.getLogger().error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + this.name + '. Key must be of type string.'); return; } if (this.annotations === undefined) { this.annotations = {}; } this.annotations[key] = value; }; /** * Adds a key-value pair to the metadata.default attribute when no namespace is given. * Metadata is not queryable, but is recorded. * @param {string} key - The name of the key to add. * @param {object|null} value - The value of the associated key. * @param {string} [namespace] - The property name to put the key/value pair under. */ Subsegment.prototype.addMetadata = function(key, value, namespace) { if (typeof key !== 'string') { logger.getLogger().error('Failed to add metadata key: ' + key + ' value: ' + value + ' to subsegment ' + this.name + '. Key must be of type string.'); return; } if (namespace && typeof namespace !== 'string') { logger.getLogger().error('Failed to add metadata key: ' + key + ' value: ' + value + ' to subsegment ' + this.name + '. Namespace must be of type string.'); return; } var ns = namespace || 'default'; if (!this.metadata) { this.metadata = {}; } if (!this.metadata[ns]) { this.metadata[ns] = {}; } if (ns !== '__proto__') { this.metadata[ns][key] = value !== null && value !== undefined ? value : ''; } }; Subsegment.prototype.addSqlData = function addSqlData(sqlData) { this.sql = sqlData; }; /** * Adds an error with associated data into the subsegment. * To handle propagating errors, the subsegment also sets a copy of the error on the * root segment. As the error passes up the execution stack, a reference is created * on each subsegment to the originating subsegment. * @param {Error|string} err - The error to capture. * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. */ Subsegment.prototype.addError = function addError(err, remote) { if (err == null || typeof err !== 'object' && typeof(err) !== 'string') { logger.getLogger().error('Failed to add error:' + err + ' to subsegment "' + this.name + '". Not an object or string literal.'); return; } this.addFaultFlag(); if (this.segment && this.segment.exception) { if (err === this.segment.exception.ex) { this.fault = true; this.cause = { id: this.segment.exception.cause, exceptions: [] }; return; } delete this.segment.exception; } if (this.segment) { this.segment.exception = { ex: err, cause: this.id }; } else { //error, cannot propagate exception if not added to segment } if (this.cause === undefined) { this.cause = { working_directory: process.cwd(), exceptions: [] }; } this.cause.exceptions.unshift(new CapturedException(err, remote)); }; /** * Adds data for an outgoing HTTP/HTTPS call. * @param {http.ClientRequest/https.ClientRequest} req - The request object from the HTTP/HTTPS call. * @param {http.IncomingMessage/https.IncomingMessage} res - The response object from the HTTP/HTTPS call. * @param {boolean} downstreamXRayEnabled - when true, adds a "traced": true hint to generated subsegments such that the AWS X-Ray service expects a corresponding segment from the downstream service. */ Subsegment.prototype.addRemoteRequestData = function addRemoteRequestData(req, res, downstreamXRayEnabled) { this.http = new RemoteRequestData(req, res, downstreamXRayEnabled); if ('traced' in this.http.request) { this.traced = this.http.request.traced; delete this.http.request.traced; } }; /** * Adds fault flag to the subsegment. */ Subsegment.prototype.addFaultFlag = function addFaultFlag() { this.fault = true; }; /** * Adds error flag to the subsegment. */ Subsegment.prototype.addErrorFlag = function addErrorFlag() { this.error = true; }; /** * Adds throttle flag to the subsegment. */ Subsegment.prototype.addThrottleFlag = function addThrottleFlag() { this.throttle = true; }; /** * Closes the current subsegment. This automatically captures any exceptions and sets the end time. * @param {Error|string} [err] - The error to capture. * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. */ Subsegment.prototype.close = function close(err, remote) { var root = this.segment; this.end_time = SegmentUtils.getCurrentTime(); delete this.in_progress; if (err) { this.addError(err, remote); } if (this.parent) { this.parent.decrementCounter(); } if (root && root.counter > SegmentUtils.getStreamingThreshold()) { if (this.streamSubsegments() && this.parent) { this.parent.removeSubsegment(this); } } }; /** * Each subsegment holds a counter of open subsegments. This increments * the counter such that it can be called from a child and propagate up. * @param {Number} [additional] - An additional amount to increment. Used when adding subsegment trees. */ Subsegment.prototype.incrementCounter = function incrementCounter(additional) { this.counter = additional ? this.counter + additional + 1 : this.counter + 1; if (this.parent) { this.parent.incrementCounter(additional); } }; /** * Each subsegment holds a counter of its open subsegments. This decrements * the counter such that it can be called from a child and propagate up. */ Subsegment.prototype.decrementCounter = function decrementCounter() { this.counter--; if (this.parent) { this.parent.decrementCounter(); } }; /** * Returns a boolean indicating whether or not the subsegment has been closed. * @returns {boolean} - Returns true if the subsegment is closed. */ Subsegment.prototype.isClosed = function isClosed() { return !this.in_progress; }; /** * Sends the subsegment to the daemon. */ Subsegment.prototype.flush = function flush() { if (!this.parent || !this.segment) { logger.getLogger().error('Failed to flush subsegment: ' + this.name + '. Subsegment must be added ' + 'to a segment chain to flush.'); return; } if (this.segment.trace_id) { if (this.segment.notTraced !== true && !this.notTraced) { SegmentEmitter.send(this); } else { logger.getLogger().debug('Ignoring flush on subsegment ' + this.id + '. Associated segment is marked as not sampled.'); } } else { logger.getLogger().debug('Ignoring flush on subsegment ' + this.id + '. Associated segment is missing a trace ID.'); } }; /** * Returns true if the subsegment was streamed in its entirety */ Subsegment.prototype.streamSubsegments = function streamSubsegments() { if (this.isClosed() && this.counter <= 0) { this.flush(); return true; } else if (this.subsegments && this.subsegments.length > 0) { var open = []; this.subsegments.forEach(function(child) { if (!child.streamSubsegments()) { open.push(child); } }); this.subsegments = open; } }; /** * Returns the formatted, trimmed subsegment JSON string to send to the daemon. */ Subsegment.prototype.format = function format() { this.type = 'subsegment'; if (this.parent) { this.parent_id = this.parent.id; } if (this.segment) { this.trace_id = this.segment.trace_id; } return this.serialize(); }; /** * Returns the formatted subsegment JSON string. */ Subsegment.prototype.toString = function toString() { return this.serialize(); }; Subsegment.prototype.toJSON = function toJSON() { var ignore = ['segment', 'parent', 'counter']; if (this.subsegments == null || this.subsegments.length === 0) { ignore.push('subsegments'); } var thisCopy = Utils.objectWithoutProperties( this, ignore, false ); return thisCopy; }; /** * Returns the serialized subsegment JSON string, replacing any BigInts with strings. */ Subsegment.prototype.serialize = function serialize(object) { return JSON.stringify( object ?? this, SegmentUtils.getJsonStringifyReplacer() ); }; module.exports = Subsegment;