UNPKG

atatus-nodejs

Version:

Atatus APM agent for Node.js

804 lines (668 loc) 26 kB
'use strict' const util = require('util') const LimitedPriorityQueue = require('../lib/priorityqueue') const Queue = require('../lib/queue') const ERROR_EVENTS_LIMIT = 20 const ERROR_REQUESTS_LIMIT = 20 const TRACE_EVENTS_LIMIT = 5 let ANALYTICS_EVENTS_LIMIT = 10000 const KIND_TRANSACTION = 'transaction' const TYPE_NODEJS = 'Node.js' const TIMEOUT_DURATION = 5 * 60 * 1000 // 5 minutes module.exports = Aggregator function Aggregator(agent) { this.agent = agent this.transactionSpans = {} this.transactions = {} this.errorMetrics = {} this.errorEvents = new Queue(ERROR_EVENTS_LIMIT) this.errorRequests = new Queue(ERROR_REQUESTS_LIMIT) this.traceEvents = new LimitedPriorityQueue(TRACE_EVENTS_LIMIT) this.metrics = [] ANALYTICS_EVENTS_LIMIT = agent._conf.maxAnalyticsEvents || ANALYTICS_EVENTS_LIMIT this.analyticsEvents = new LimitedPriorityQueue(ANALYTICS_EVENTS_LIMIT) this.analyticsEventsSize = 0 } function _getSpanType (spanType, subtype, action) { if (spanType === TYPE_NODEJS) { return { kind: TYPE_NODEJS, type: TYPE_NODEJS, knownKind: false } } // let spanTypes = spanType.split('.') let availableKinds = { 'db': 'Database', 'cache': 'Database', 'ext': 'Remote', 'external': 'Remote', 'websocket': 'Remote', 'template': 'Template' } let availableTypes = { 'mysql': 'MySQL', 'postgresql': 'Postgres', 'mssql': 'MS SQL', 'mongodb': 'MongoDB', 'redis': 'Redis', 'graphql': 'GraphQL', 'elasticsearch': 'Elasticsearch', 'cassandra': 'Cassandra', 'http': 'External Requests', 'https': 'External Requests', 'http2': 'External Requests' } let kind = availableKinds[spanType] || spanType let type = availableTypes[subtype] || subtype if (spanType === 'websocket') { type = 'WebSocket' } else if (spanType === 'template') { type = 'Template' } return { kind: kind, type: type || kind, knownKind: !!availableKinds[spanType] } } // let debugTxnMap = {} Aggregator.prototype.addTxn = function (item) { let txnName = item.name; if (!txnName) { return; } // debugTxnMap[item.id] = new Date().getTime(); item.duration = item.duration || 0 let txn = this.transactions[txnName] if (!txn) { txn = { name: txnName, background: (item.type === 'background'), // [count, total, min, max ] durations: [ 0, 0, item.duration, item.duration ], kind: KIND_TRANSACTION, type: this.agent._conf.frameworkName, spansObject: {} } } this.transactions[txnName] = txn // [count, total, min, max ] txn.durations[0]++ txn.durations[1] += item.duration txn.durations[2] = Math.min(item.duration, txn.durations[2]) txn.durations[3] = Math.max(item.duration, txn.durations[3]) if (!this.transactionSpans[item.id]) { this.transactionSpans[item.id] = { spansArray: [] } } if (this.transactionSpans[item.id].timeoutId) { clearTimeout(this.transactionSpans[item.id].timeoutId) } let totalSpanTime = 0 let analytics = this.agent._conf.analytics && !!(this.agent._conf.backendConfig && this.agent._conf.backendConfig.analytics) let analyticsCaptureOutgoing = this.agent._conf.analyticsCaptureOutgoing let analyticsSpan = { databaseCount: 0, remoteCount: 0, databaseDur: 0, remoteDur: 0 } let aggregatorInstance = this this.transactionSpans[item.id].spansArray.forEach(function(span) { let spanName = span.name if (!spanName) { return; } span.duration = span.duration || 0 let spanObj = txn.spansObject[spanName] if (!spanObj) { spanObj = { name: spanName, // durations: [count, total, min, max ] durations: [ 0, 0, span.duration, span.duration ] } } txn.spansObject[spanName] = spanObj let spanTypes = _getSpanType(span.type, span.subtype, span.action) spanObj.type = spanTypes.type spanObj.kind = spanTypes.kind if (analytics && analyticsCaptureOutgoing && (spanObj.kind === "Remote") && (spanObj.type !== "WebSocket")) { addAnalyticsOutgoingData(span, item, aggregatorInstance) } // Only if analytics is enabled if (analytics) { if (spanObj.kind === "Database") { analyticsSpan.databaseCount++ analyticsSpan.databaseDur += span.duration } if (spanObj.kind === "Remote") { analyticsSpan.remoteCount++ analyticsSpan.remoteDur += span.duration } } // To avoid adding custom span duration in total span time. if (spanTypes.knownKind) { totalSpanTime += span.duration } // [count, total, min, max ] spanObj.durations[0]++ spanObj.durations[1] += span.duration spanObj.durations[2] = Math.min(span.duration, spanObj.durations[2]) spanObj.durations[3] = Math.max(span.duration, spanObj.durations[3]) }) // Calculate nodejs span if (totalSpanTime < item.duration) { let duration = item.duration - totalSpanTime let spanName = this.agent._conf.frameworkName || TYPE_NODEJS let spanObj = txn.spansObject[spanName] if (!spanObj) { spanObj = { name: spanName, // durations: [count, total, min, max ] durations: [ 0, 0, duration, duration ] } } txn.spansObject[spanName] = spanObj if (spanObj.durations[0] === 0) { spanObj.type = TYPE_NODEJS spanObj.kind = TYPE_NODEJS } // [count, total, min, max ] spanObj.durations[0]++ spanObj.durations[1] += duration spanObj.durations[2] = Math.min(duration, spanObj.durations[2]) spanObj.durations[3] = Math.max(duration, spanObj.durations[3]) } if (item.duration >= this.agent._conf.traceThreshold) { item.spansArray = this.transactionSpans[item.id].spansArray if (totalSpanTime < item.duration) { item.spansArray.unshift({ transaction_id: item.id, parent_id: item.id, trace_id: item.trace_id, name: this.agent._conf.frameworkName || TYPE_NODEJS, type: TYPE_NODEJS, duration: item.duration - totalSpanTime, }) } this.traceEvents.add(item) } // Only if analytics is enabled if (analytics) { addAnalyticsData(item, analyticsSpan, this) } delete this.transactionSpans[item.id] } // Add analytics outgoing request payload function addAnalyticsOutgoingData (span, currentTransaction, aggregatorInstance) { currentTransaction = currentTransaction || {} let txnContext = currentTransaction.context || {} let user = txnContext.user || {} let company = txnContext.company || {} let txnRequest = txnContext.request || {} let txnRequestHeader = txnRequest.headers || {} let ip = txnRequestHeader['x-forwarded-for'] || txnRequestHeader['X-Forwarded-For'] || (txnRequest.socket && txnRequest.socket.remote_address) || '' ip = ip.split(',')[0] || '' let context = (span.context && span.context.http) || {} let request = context.request || {} let response = context.response || {} let requestBody = request.body || '' let responseBody = response.body || '' let requestHeaders = request.headers || {} let responseHeaders = context.responseHeaders || {} let bodyLength = ((requestBody.length || 0) + (responseBody.length || 0)) || 0 let payload = { timestamp: currentTransaction.timestamp ? Math.floor(currentTransaction.timestamp / 1000) : new Date().getTime(), txnId: span.transaction_id || span.id, trace_id: span.trace_id, name: span.name || span.transaction_name, duration: span.duration, requestHeaders: requestHeaders, responseHeaders: responseHeaders, requestBody: requestBody, responseBody: responseBody, bodyLength: bodyLength, customData: txnContext.custom || {}, url: context.url || '', statusCode: context.statusCode || 200, method: context.method || 'GET', userAgent: requestHeaders['user-agent'], ip: ip, direction: 'outgoing', userId: user.id, userName: user.name, userEmail: user.email, companyId: company.id, } try { if (aggregatorInstance.agent._conf.analyticsSkip && aggregatorInstance.agent._conf.analyticsSkip(payload)) { aggregatorInstance.agent.logger.debug('Skipped analytics outgoing event: ' + payload.name) return } } catch (e) { aggregatorInstance.agent.logger.error('Analytics skip function failed with exception: %s', e.message) } try { if (aggregatorInstance.agent._conf.analyticsMask) { aggregatorInstance.agent._conf.analyticsMask(payload) } } catch (e) { aggregatorInstance.agent.logger.error('Analytics mask function failed with exception: %s', e.message) } // Add payload to events aggregatorInstance.analyticsEvents.add(payload) aggregatorInstance.analyticsEventsSize += (payload.bodyLength || 0) } function addAnalyticsData (item, analyticsSpan, aggregatorInstance) { let request = _getRequestPayload(item.context) let user = (item.context && item.context.user) || {} let company = (item.context && item.context.company) || {} // For custom start transaction, there wont be any status code and url. if (!request.statusCode && !request.url) { return; } let payload = { timestamp: item.timestamp ? Math.floor(item.timestamp / 1000) : new Date().getTime(), txnId: item.transaction_id || item.id, // kind: KIND_TRANSACTION, // type: TYPE_NODEJS, name: item.name || item.transaction_name, duration: item.duration, dbCalls: analyticsSpan.databaseCount, dbDuration: analyticsSpan.databaseDur, extCalls: analyticsSpan.remoteCount, extDuration: analyticsSpan.remoteDur, errorClass: analyticsSpan.errClass || '', errorMessage: analyticsSpan.errMsg || '', customData: item.context && item.context.custom, // tags: item.context && item.context.tags && Object.values(item.context.tags), direction: 'incoming', userId: user.id, userName: user.name, userEmail: user.email, companyId: company.id, ...request } try { if (aggregatorInstance.agent._conf.analyticsSkip && aggregatorInstance.agent._conf.analyticsSkip(payload)) { aggregatorInstance.agent.logger.debug('Skipped analytics event: ' + payload.name) return } } catch (e) { aggregatorInstance.agent.logger.error('Analytics skip function failed with exception: %s', e.message) } try { if (aggregatorInstance.agent._conf.analyticsMask) { aggregatorInstance.agent._conf.analyticsMask(payload) } } catch (e) { aggregatorInstance.agent.logger.error('Analytics mask function failed with exception: %s', e.message) } // Add payload to events aggregatorInstance.analyticsEvents.add(payload) aggregatorInstance.analyticsEventsSize += (payload.bodyLength || 0) } Aggregator.prototype.addSpan = function (item) { // if (debugTxnMap[item.transaction_id]) { // this.agent.logger.debug(`Span late - ${item.name} - ${new Date().getTime() - debugTxnMap[item.transaction_id]} ms`) // } let spanName = item.name let transaction_id = item.transaction_id if (!spanName || (this.agent._conf.serverHosts && this.agent._conf.serverHosts.indexOf(spanName) !== -1)) { return; } this.transactionSpans[transaction_id] = this.transactionSpans[transaction_id] || { spansArray: null }; let spansArray = this.transactionSpans[transaction_id].spansArray; if (spansArray) { spansArray.push(item) } else { this.transactionSpans[transaction_id].spansArray = [ item ] // Clear spans after certain duration let timeoutId = setTimeout( () => { if (this.transactionSpans[transaction_id]) { delete this.transactionSpans[transaction_id]; } }, TIMEOUT_DURATION); this.transactionSpans[transaction_id].timeoutId = timeoutId } } Aggregator.prototype.addErrorEvents = function (item) { let exception = item.exception || item.log if (!exception) { return; } let error = { timestamp: new Date().getTime(), transaction: item.transaction_name, request: _getRequestHeader(item.context), customData: item.context && item.context.custom, tags: item.context && item.context.tags && Object.values(item.context.tags), user: item.context && item.context.user } error.exceptions = [{ class: exception.type || 'Error', message: exception.message, stacktrace: [] }] // let errSpan = { // errClass: error.exceptions[0].class, // errMsg: error.exceptions[0].message // } exception.stacktrace = exception.stacktrace || [] exception.stacktrace.forEach(function(callSite) { // Ignore if file name is containing atatus nodejs, anonymous or no file name let fileName = callSite.filename if (!fileName || fileName.indexOf("/atatus-nodejs/") !== -1 || fileName === "<anonymous>") { return; } let frame = { f: fileName, p: callSite.abs_path, m: callSite.function || "none", ln: callSite.lineno, inp: !callSite.library_frame }; error.exceptions[0].stacktrace.push(frame) }); // Only if analytics is enabled // if (this.agent._conf.analytics) { // addAnalyticsData(item, errSpan, this) // } this.errorEvents.add(error) } Aggregator.prototype.addErrorMetrics = function (statusCode, item) { let txnName = item.name; if (!txnName || !statusCode) { return; } let errorMetric = this.errorMetrics[item.name] if (!errorMetric) { errorMetric = { name: txnName, kind: "transaction", type: this.agent._conf.frameworkName, statusCodes: {} } } let requestHeader = item.context && item.context.request && item.context.request.headers || {} this.errorMetrics[item.name] = errorMetric errorMetric.statusCodes[statusCode] = (errorMetric.statusCodes[statusCode] || 0) + 1; let errorRequests = { name: txnName, type: this.agent._conf.frameworkName, kind: KIND_TRANSACTION, request: _getRequestHeader(item.context), customData: item.context && item.context.custom, tags: item.context && item.context.tags && Object.values(item.context.tags), user: item.context && item.context.user } this.errorRequests.add(errorRequests) } Aggregator.prototype.addMetric = function (item) { if (!item || !item.samples) { return } let samples = item.samples let metric = { cpu: { total: (samples['system.cpu.total.norm.pct'] && samples['system.cpu.total.norm.pct'].value) || 0 }, memory: { total: (samples['system.memory.total'] && samples['system.memory.total'].value) || 0, actualFree: (samples['system.memory.actual.free'] && samples['system.memory.actual.free'].value) || 0 }, process: { cpu: { total: (samples['system.process.cpu.total.norm.pct'] && samples['system.process.cpu.total.norm.pct'].value) || 0, system: (samples['system.process.cpu.system.norm.pct'] && samples['system.process.cpu.system.norm.pct'].value) || 0, user: (samples['system.process.cpu.user.norm.pct'] && samples['system.process.cpu.user.norm.pct'].value) || 0 }, memory: { rssBytes: (samples['system.process.memory.rss.bytes'] && samples['system.process.memory.rss.bytes'].value) || 0 } }, nodejs: { handlesActive: (samples['nodejs.handles.active'] && samples['nodejs.handles.active'].value) || 0, requestsActive: (samples['nodejs.requests.active'] && samples['nodejs.requests.active'].value) || 0, eventLoopDelay: (samples['nodejs.eventloop.delay.avg.ms'] && samples['nodejs.eventloop.delay.avg.ms'].value) || 0, memoryHeapAllocated: (samples['nodejs.memory.heap.allocated.bytes'] && samples['nodejs.memory.heap.allocated.bytes'].value) || 0, memoryHeapUsed: (samples['nodejs.memory.heap.used.bytes'] && samples['nodejs.memory.heap.used.bytes'].value) || 0 }, timestamp: item.timestamp ? Math.floor(item.timestamp / 1000) : new Date().getTime() } // drop memory value is 0 if (metric.memory.total === 0) { return } this.metrics.push(metric) } // Get request and response payload for analytics function _getRequestPayload (context) { let request = (context && context.request) || {} let response = (context && context.response) || {} let requestBody = request.body || '' let responseBody = (context && context.responseBody) || response.body || '' let requestHeader = request.headers || {} let ip = requestHeader['x-forwarded-for'] || requestHeader['X-Forwarded-For'] || (request.socket && request.socket.remote_address) || '' ip = ip.split(',')[0] || '' let bodyLength = ((requestBody.length || 0) + (responseBody.length || 0)) || 0 return { statusCode: response.status_code, method: request.method, url: (request.url && (request.url.full || request.url.raw)) || '', userAgent: requestHeader['user-agent'], ip: ip, requestHeaders: request.headers || {}, responseHeaders: response.headers || {}, requestBody: requestBody, responseBody: responseBody, bodyLength: bodyLength // host: request.url && request.url.hostname, // port: (request.url && request.url.port && parseInt(request.url.port)) || 0 } } function _getRequestHeader (context) { let request = context && context.request || {} let requestHeader = request && request.headers || {} let response = context && context.response || {} let ip = requestHeader['x-forwarded-for'] || requestHeader['X-Forwarded-For'] || (request.socket && request.socket.remote_address) || '' ip = ip.split(',')[0] || '' return { accept: requestHeader.accept, 'accept-encoding': requestHeader['accept-encoding'], 'accept-language': requestHeader['accept-language'], ip: ip, referer: requestHeader.referer, host: requestHeader.host, port: (request.url && request.url.port && parseInt(request.url.port)) || 0, method: request.method, userAgent: requestHeader['user-agent'], path: (request.url && request.url.raw) || '', statusCode: response.status_code } } Aggregator.prototype.getAndResetTracesPayload = function getTracesPayload () { let traceEvents = this.traceEvents.toArray() || [] this.resetTraces() let tracesPayload = [] traceEvents.forEach((event) => { let txnName = event.name; if (!txnName) { return; } let trace = { name: txnName, id: event.id, type: this.agent._conf.frameworkName, kind: KIND_TRANSACTION, // start: 389882784298, duration: event.duration, entries: [], funcs: [], request: _getRequestHeader(event.context), customData: event.context && event.context.custom, tags: event.context && event.context.tags && Object.values(event.context.tags), user: event.context && event.context.user } event.spansArray.forEach((span, index) => { let spanTypes = _getSpanType(span.type, span.subtype, span.action) let traceEntry = { i: index, lv: 1, //span.level || so: span.startOffset || 0, du: span.duration, ly: { name: span.name, type: spanTypes.type, kind: spanTypes.kind }, } if (span.context && span.context.db) { traceEntry.dt = { // dns: "", query: span.context.db.statement } } else if (span.context && span.context.http) { traceEntry.dt = { url: span.context.http.url } } trace.entries.push(traceEntry) trace.funcs.push(span.name) }); // let idFinderObject = {} // let multiDimensionArray = [] // trace.entries.forEach((span, index) => { // let indexToPush = idFinderObject[span.parent_id] // if (span.level === 1) { // multiDimensionArray.push( [ span ] ) // idFinderObject[span.parent_id] = span.parent_id // } else { // } // } // let transaction_id = event.transaction_id // for (let i = event.spansArray.length - 1; i >= 0; i--) { // if (event.spansArray[i].parent_id != transaction_id && event.spansArray[i + 1]) { // event.spansArray[i].lv = event.spansArray[i + 1].lv + 1 // } // } tracesPayload.push(trace) }); return { startTime: new Date().getTime() - (60*1000), endTime: new Date().getTime(), traces: tracesPayload } } Aggregator.prototype.getAndResetTransactionsPayload = function () { let transactions = this.transactions this.resetTransactions() let txnsPayload = [] for (let txnName of Object.keys(transactions)) { let txn = transactions[txnName] txn.traces = Object.values(txn.spansObject) delete txn.spansObject txnsPayload.push(txn) } return { startTime: new Date().getTime() - (60*1000), endTime: new Date().getTime(), transactions: txnsPayload } } Aggregator.prototype.getAndResetAnalyticsPayload = function () { let analyticsEvents = this.analyticsEvents.toArray() || [] let analyticsEventsSize = this.analyticsEventsSize || 0 this.resetAnalyticsEvents() return { startTime: new Date().getTime() - (60*1000), endTime: new Date().getTime(), requests: analyticsEvents, size: analyticsEventsSize } } Aggregator.prototype.getAndResetErrorEventsPayload = function () { let errorEvents = this.errorEvents.toArray() this.resetErrorEvents() return { errors: errorEvents } } Aggregator.prototype.getAndResetErrorMetricsPayload = function () { let errorMetrics = Object.values(this.errorMetrics) let errorRequests = this.errorRequests.toArray() this.resetErrorMetrics() return { startTime: new Date().getTime() - (60*1000), endTime: new Date().getTime(), errorMetrics: errorMetrics, errorRequests: errorRequests } } Aggregator.prototype.getAndResetMetrics = function () { let metrics = this.metrics this.resetMetrics() return { startTime: new Date().getTime() - (60*1000), endTime: new Date().getTime(), metrics: { nodejs: metrics, } } } Aggregator.prototype.getAndResetHostInfoPayload = function () { return { } } Aggregator.prototype.resetTransactions = function () { this.transactions = {} } Aggregator.prototype.resetAnalyticsEvents = function () { this.analyticsEvents = new LimitedPriorityQueue(ANALYTICS_EVENTS_LIMIT) this.analyticsEventsSize = 0 } Aggregator.prototype.resetTraces = function () { this.traceEvents = new LimitedPriorityQueue(TRACE_EVENTS_LIMIT) } Aggregator.prototype.resetErrorEvents = function () { this.errorEvents = new Queue(ERROR_EVENTS_LIMIT) } Aggregator.prototype.resetErrorMetrics = function () { this.errorMetrics = {} this.errorRequests = new Queue(ERROR_REQUESTS_LIMIT) } Aggregator.prototype.resetMetrics = function () { this.metrics = [] } Aggregator.prototype.mergeTransactions = function(payload) { } Aggregator.prototype.mergeAnalyticsEvents = function(payload) { } Aggregator.prototype.mergeTraces = function(payload) { } Aggregator.prototype.mergeErrorEventsPayload = function(payload) { } Aggregator.prototype.mergeErrorMetrics = function(payload) { } Aggregator.prototype.mergeMetrics = function(payload) { } Aggregator.prototype.isTransactionsEmpty = function () { return (Object.keys(this.transactions).length < 1) } Aggregator.prototype.isAnalyticsEmpty = function () { return (this.analyticsEvents.length < 1) } Aggregator.prototype.isTracesEmpty = function () { return (this.traceEvents.length < 1) } Aggregator.prototype.isErrorEventsEmpty = function () { return (this.errorEvents.length < 1) } Aggregator.prototype.isErrorMetricsEmpty = function () { return (this.errorRequests.length < 1) } Aggregator.prototype.isMetricsEmpty = function () { return (this.metrics.length < 1) }