diff --git a/README.md b/README.md index de6acf0..8757c90 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ agent.start({ }); ``` -note that all options given (including empty/null values) will override the corresponding default values, e.g. `agent.start({ collectorAddress: '' })` will override the default value of `collectorAddress` to empty string, causing errors like `DNS resolution failed`. +Note that all options given (including empty/null values) will override the corresponding default values, e.g. `agent.start({ collectorAddress: '' })` will override the default value of `collectorAddress` to empty string, causing errors like `DNS resolution failed`. - Use environment variables. @@ -66,6 +66,8 @@ Environment Variable | Description | Default | `SW_MONGO_PARAMETERS_MAX_LENGTH` | The maximum string length of mongodb parameters to log | `512` | | `SW_AGENT_MAX_BUFFER_SIZE` | The maximum buffer size before sending the segment data to backend | `'1000'` | +Note that the various ignore options like `SW_IGNORE_SUFFIX`, `SW_TRACE_IGNORE_PATH` and `SW_HTTP_IGNORE_METHOD` as well as endpoints which are not recorded due to exceeding `SW_AGENT_MAX_BUFFER_SIZE` all propagate their ignored status downstream to any other endpoints they may call. If that endpoint is running the Node Skywalking agent then regardless of its ignore settings it will not be recorded since its upstream parent was not recorded. This allows elimination of entire trees of endpoints you are not interested in as well as eliminating partial traces if a span in the chain is ignored but calls out to other endpopints which are recorded as children of ROOT instead of the actual parent. + ## Supported Libraries There are some built-in plugins that support automatic instrumentation of NodeJS libraries, the complete lists are as follows: diff --git a/src/agent/Buffer.ts b/src/agent/Buffer.ts deleted file mode 100644 index 99a18fb..0000000 --- a/src/agent/Buffer.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*! - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * - */ - -import { createLogger } from '../logging'; -import config from '../config/AgentConfig'; - -const logger = createLogger(__filename); - -export default class Buffer { - private readonly maxSize: number; - private readonly buffer: T[]; - - constructor() { - this.maxSize = config.maxBufferSize; - this.buffer = []; - } - - get length(): number { - return this.buffer.length; - } - - put(element: T): boolean { - if (this.length > this.maxSize) { - logger.warn('Drop the data because of the buffer is oversize'); - return false; - } - this.buffer.push(element); - - return true; - } - - take(): T { - return this.buffer.splice(0, 1)[0]; - } -} diff --git a/src/agent/protocol/grpc/clients/TraceReportClient.ts b/src/agent/protocol/grpc/clients/TraceReportClient.ts index 79b19bc..810bff2 100755 --- a/src/agent/protocol/grpc/clients/TraceReportClient.ts +++ b/src/agent/protocol/grpc/clients/TraceReportClient.ts @@ -24,7 +24,6 @@ import { createLogger } from '../../../../logging'; import Client from './Client'; import { TraceSegmentReportServiceClient } from '../../../../proto/language-agent/Tracing_grpc_pb'; import AuthInterceptor from '../AuthInterceptor'; -import Buffer from '../../../../agent/Buffer'; import SegmentObjectAdapter from '../SegmentObjectAdapter'; import { emitter } from '../../../../lib/EventEmitter'; import Segment from '../../../../trace/context/Segment'; @@ -33,20 +32,18 @@ const logger = createLogger(__filename); export default class TraceReportClient implements Client { private readonly reporterClient: TraceSegmentReportServiceClient; - private readonly buffer: Buffer; + private readonly buffer: Segment[] = []; private timeout?: NodeJS.Timeout; constructor() { - this.buffer = new Buffer(); this.reporterClient = new TraceSegmentReportServiceClient( config.collectorAddress, config.secure ? grpc.credentials.createSsl() : grpc.credentials.createInsecure(), { interceptors: [AuthInterceptor] }, ); emitter.on('segment-finished', (segment) => { - if (this.buffer.put(segment)) { - this.timeout?.ref(); - } + this.buffer.push(segment); + this.timeout?.ref(); }); } @@ -56,6 +53,8 @@ export default class TraceReportClient implements Client { start() { const reportFunction = () => { + emitter.emit('segments-sent'); // reset limiter in SpanContext + try { if (this.buffer.length === 0) { return; @@ -67,15 +66,18 @@ export default class TraceReportClient implements Client { } }); - while (this.buffer.length > 0) { - const segment = this.buffer.take(); - if (segment) { - if (logger.isDebugEnabled()) { - logger.debug('Sending segment ', { segment }); - } + try { + for (const segment of this.buffer) { + if (segment) { + if (logger._isDebugEnabled) { + logger.debug('Sending segment ', { segment }); + } - stream.write(new SegmentObjectAdapter(segment)); + stream.write(new SegmentObjectAdapter(segment)); + } } + } finally { + this.buffer.length = 0; } stream.end(); diff --git a/src/core/PluginInstaller.ts b/src/core/PluginInstaller.ts index 07f3e57..10d4bc6 100644 --- a/src/core/PluginInstaller.ts +++ b/src/core/PluginInstaller.ts @@ -28,7 +28,12 @@ const logger = createLogger(__filename); let topModule = module; while (topModule.parent) { + const filename = topModule.filename; + topModule = topModule.parent; + + if (filename.endsWith('/skywalking-nodejs/lib/index.js')) // stop at the appropriate level in case app is being run by some other framework + break; } export default class PluginInstaller { @@ -88,7 +93,7 @@ export default class PluginInstaller { const pluginFile = path.join(this.pluginDir, file); try { - plugin = require(pluginFile).default as SwPlugin; + plugin = this.require(pluginFile).default as SwPlugin; const { isSupported, version } = this.checkModuleVersion(plugin); if (!isSupported) { diff --git a/src/lib/EventEmitter.ts b/src/lib/EventEmitter.ts index 9d4f708..c801979 100644 --- a/src/lib/EventEmitter.ts +++ b/src/lib/EventEmitter.ts @@ -24,6 +24,7 @@ import Segment from '../trace/context/Segment'; declare interface SkyWalkingEventEmitter { on(event: 'segment-finished', listener: (segment: Segment) => void): this; + on(event: 'segments-sent', listener: () => void): this; } class SkyWalkingEventEmitter extends EventEmitter { diff --git a/src/logging/index.ts b/src/logging/index.ts index f689a7a..68c0302 100644 --- a/src/logging/index.ts +++ b/src/logging/index.ts @@ -21,12 +21,12 @@ import * as winston from 'winston'; import { Logger } from 'winston'; type LoggerLevelAware = Logger & { - isDebugEnabled(): boolean; - isInfoEnabled(): boolean; + _isDebugEnabled: boolean; + _isInfoEnabled: boolean; }; export function createLogger(name: string): LoggerLevelAware { - const loggingLevel = process.env.SW_AGENT_LOGGING_LEVEL || (process.env.NODE_ENV !== 'production' ? 'debug' : 'info'); + const loggingLevel = (process.env.SW_AGENT_LOGGING_LEVEL || 'error').toLowerCase(); const logger = winston.createLogger({ level: loggingLevel, @@ -35,6 +35,7 @@ export function createLogger(name: string): LoggerLevelAware { file: name, }, }); + if (process.env.NODE_ENV !== 'production' || process.env.SW_LOGGING_TARGET === 'console') { logger.add( new winston.transports.Console({ @@ -49,11 +50,25 @@ export function createLogger(name: string): LoggerLevelAware { ); } - const isDebugEnabled = (): boolean => logger.levels[logger.level] >= logger.levels.debug; - const isInfoEnabled = (): boolean => logger.levels[logger.level] >= logger.levels.info; + const loggerLevel = logger.levels[logger.level]; + const _isDebugEnabled = loggerLevel >= logger.levels.debug; + const _isInfoEnabled = loggerLevel >= logger.levels.info; + + Object.assign(logger, { + _isDebugEnabled, + _isInfoEnabled, + }); + + const nop = (): void => { /* a cookie for the linter */ }; + + if (loggerLevel < logger.levels.debug) // we do this because logger still seems to stringify anything sent to it even if it is below the logging level, costing performance + (logger as any).debug = nop; + + if (loggerLevel < logger.levels.info) + (logger as any).info = nop; + + if (loggerLevel < logger.levels.warn) + (logger as any).warn = nop; - return Object.assign(logger, { - isDebugEnabled, - isInfoEnabled, - } as LoggerLevelAware); + return logger as LoggerLevelAware; } diff --git a/src/plugins/AMQPLibPlugin.ts b/src/plugins/AMQPLibPlugin.ts index 079b0bc..fe91742 100644 --- a/src/plugins/AMQPLibPlugin.ts +++ b/src/plugins/AMQPLibPlugin.ts @@ -43,7 +43,7 @@ class AMQPLibPlugin implements SwPlugin { const topic = fields.exchange || ''; const queue = fields.routingKey || ''; const peer = `${this.connection.stream.remoteAddress}:${this.connection.stream.remotePort}`; - const span = ContextManager.current.newExitSpan('RabbitMQ/' + topic + '/' + queue + '/Producer', peer, Component.RABBITMQ_PRODUCER); + const span = ContextManager.current.newExitSpan('RabbitMQ/' + topic + '/' + queue + '/Producer', Component.RABBITMQ_PRODUCER); span.start(); diff --git a/src/plugins/AxiosPlugin.ts b/src/plugins/AxiosPlugin.ts index 1eedd67..a335b69 100644 --- a/src/plugins/AxiosPlugin.ts +++ b/src/plugins/AxiosPlugin.ts @@ -49,7 +49,7 @@ class AxiosPlugin implements SwPlugin { const method = (config.method || 'GET').toUpperCase(); const span = ignoreHttpMethodCheck(method) ? DummySpan.create() - : ContextManager.current.newExitSpan(operation, host, Component.AXIOS, Component.HTTP); + : ContextManager.current.newExitSpan(operation, Component.AXIOS, Component.HTTP); span.start(); diff --git a/src/plugins/HttpPlugin.ts b/src/plugins/HttpPlugin.ts index 22c37f4..a4eba60 100644 --- a/src/plugins/HttpPlugin.ts +++ b/src/plugins/HttpPlugin.ts @@ -63,7 +63,7 @@ class HttpPlugin implements SwPlugin { const method = arguments[url instanceof URL || typeof url === 'string' ? 1 : 0]?.method || 'GET'; const span = ignoreHttpMethodCheck(method) ? DummySpan.create() - : ContextManager.current.newExitSpan(operation, host, Component.HTTP); + : ContextManager.current.newExitSpan(operation, Component.HTTP); if (span.depth) // if we inherited from a higher level plugin then do nothing, higher level should do all the work and we don't duplicate here return _request.apply(this, arguments); diff --git a/src/plugins/MongoDBPlugin.ts b/src/plugins/MongoDBPlugin.ts index 923a683..c1f70b5 100644 --- a/src/plugins/MongoDBPlugin.ts +++ b/src/plugins/MongoDBPlugin.ts @@ -291,7 +291,7 @@ class MongoDBPlugin implements SwPlugin { host = db.serverConfig.s.options.servers.map((s: any) => `${s.host}:${s.port}`).join(','); // will this work for non-NativeTopology? } catch { /* nop */ } - span = ContextManager.current.newExitSpan('MongoDB/' + operation, host, Component.MONGODB); + span = ContextManager.current.newExitSpan('MongoDB/' + operation, Component.MONGODB); span.start(); diff --git a/src/plugins/MongoosePlugin.ts b/src/plugins/MongoosePlugin.ts index d8f5fd2..0b2f458 100644 --- a/src/plugins/MongoosePlugin.ts +++ b/src/plugins/MongoosePlugin.ts @@ -98,7 +98,7 @@ class MongoosePlugin implements SwPlugin { return _original.apply(this, arguments); const host = `${this.db.host}:${this.db.port}`; - span = ContextManager.current.newExitSpan('Mongoose/' + operation, host, Component.MONGOOSE, Component.MONGODB); + span = ContextManager.current.newExitSpan('Mongoose/' + operation, Component.MONGOOSE, Component.MONGODB); span.start(); diff --git a/src/plugins/MySQLPlugin.ts b/src/plugins/MySQLPlugin.ts index 0f67b04..caf0352 100644 --- a/src/plugins/MySQLPlugin.ts +++ b/src/plugins/MySQLPlugin.ts @@ -37,7 +37,7 @@ class MySQLPlugin implements SwPlugin { let query: any; const host = `${this.config.host}:${this.config.port}`; - const span = ContextManager.current.newExitSpan('mysql/query', host, Component.MYSQL); + const span = ContextManager.current.newExitSpan('mysql/query', Component.MYSQL); span.start(); diff --git a/src/plugins/PgPlugin.ts b/src/plugins/PgPlugin.ts index 7129def..ad266d0 100644 --- a/src/plugins/PgPlugin.ts +++ b/src/plugins/PgPlugin.ts @@ -44,7 +44,7 @@ class MySQLPlugin implements SwPlugin { let query: any; const host = `${this.host}:${this.port}`; - const span = ContextManager.current.newExitSpan('pg/query', host, Component.POSTGRESQL); + const span = ContextManager.current.newExitSpan('pg/query', Component.POSTGRESQL); span.start(); diff --git a/src/trace/context/Context.ts b/src/trace/context/Context.ts index 045a07e..9928895 100644 --- a/src/trace/context/Context.ts +++ b/src/trace/context/Context.ts @@ -25,6 +25,7 @@ import { ContextCarrier } from './ContextCarrier'; export default interface Context { segment: Segment; nSpans: number; + finished: boolean; newLocalSpan(operation: string): Span; @@ -36,7 +37,7 @@ export default interface Context { /* if 'inherit' is specified then the span returned is marked for inheritance by an Exit span component which is created later and calls this function with a matching 'component' value. For example Axios using an Http exit connection will be merged into a single exit span, see those plugins for how this is done. */ - newExitSpan(operation: string, peer: string, component: Component, inherit?: Component): Span; + newExitSpan(operation: string, component: Component, inherit?: Component): Span; start(span: Span): Context; diff --git a/src/trace/context/ContextCarrier.ts b/src/trace/context/ContextCarrier.ts index 8d94053..abe06a1 100644 --- a/src/trace/context/ContextCarrier.ts +++ b/src/trace/context/ContextCarrier.ts @@ -68,17 +68,21 @@ export class ContextCarrier extends CarrierItem { } isValid(): boolean { - return ( - this.traceId !== undefined && - this.segmentId !== undefined && + return Boolean( + this.traceId?.rawId && + this.segmentId?.rawId && this.spanId !== undefined && - this.service !== undefined && - this.endpoint !== undefined && + !isNaN(this.spanId) && + this.service && + this.endpoint && this.clientAddress !== undefined ); } - public static from(map: { [key: string]: string }): ContextCarrier { + public static from(map: { [key: string]: string }): ContextCarrier | undefined { + if (!map.hasOwnProperty('sw8')) + return; + const carrier = new ContextCarrier(); carrier.items.filter((item) => map.hasOwnProperty(item.key)).forEach((item) => (item.value = map[item.key])); diff --git a/src/trace/context/ContextManager.ts b/src/trace/context/ContextManager.ts index 9f3205c..fdad6ab 100644 --- a/src/trace/context/ContextManager.ts +++ b/src/trace/context/ContextManager.ts @@ -17,13 +17,15 @@ * */ +import config from '../../config/AgentConfig'; import Context from '../../trace/context/Context'; import Span from '../../trace/span/Span'; import SpanContext from '../../trace/context/SpanContext'; +import DummyContext from '../../trace/context/DummyContext'; import async_hooks from 'async_hooks'; -type AsyncState = { spans: Span[], valid: boolean }; +type AsyncState = { spans: Span[] }; let store: { getStore(): AsyncState | undefined; @@ -58,11 +60,10 @@ if (async_hooks.AsyncLocalStorage) { class ContextManager { get asyncState(): AsyncState { - // since `AsyncLocalStorage.getStore` may get previous state, see issue https://github.com/nodejs/node/issues/35286#issuecomment-697207158, so recreate when asyncState is not valid - // Necessary because span may "finish()" in a child async task of where the asyncState was actually created and so clearing in the child would not clear in parent and invalid asyncState would be reused in new children of that parent. let asyncState = store.getStore(); - if (!asyncState?.valid) { - asyncState = { spans: [], valid: true }; + + if (!asyncState) { + asyncState = { spans: [] }; store.enterWith(asyncState); } @@ -76,13 +77,19 @@ class ContextManager { }; get hasContext(): boolean | undefined { - return store.getStore()?.valid; + return Boolean(store.getStore()?.spans.length); } get current(): Context { const asyncState = this.asyncState; - return !asyncState.spans.length ? new SpanContext() : asyncState.spans[asyncState.spans.length - 1].context; + if (asyncState.spans.length) + return asyncState.spans[asyncState.spans.length - 1].context; + + if (SpanContext.nActiveSegments < config.maxBufferSize) + return new SpanContext(); + + return new DummyContext(); } get spans(): Span[] { @@ -92,10 +99,10 @@ class ContextManager { spansDup(): Span[] { let asyncState = store.getStore(); - if (!asyncState?.valid) { - asyncState = { spans: [], valid: true }; + if (!asyncState) { + asyncState = { spans: [] }; } else { - asyncState = { spans: [...asyncState.spans], valid: asyncState.valid }; + asyncState = { spans: [...asyncState.spans] }; } store.enterWith(asyncState); @@ -103,13 +110,19 @@ class ContextManager { return asyncState.spans; } - clear(): void { - this.asyncState.valid = false; - store.enterWith(undefined as unknown as AsyncState); + clear(span: Span): void { + const spans = this.spansDup(); // this needed to make sure async tasks created before this call will still have this span at the top of their span list + const idx = spans.indexOf(span); + + if (idx !== -1) + spans.splice(idx, 1); } - restore(context: Context, spans: Span[]): void { - store.enterWith({ spans: spans || [], valid: this.asyncState.valid }); + restore(span: Span): void { + const spans = this.spansDup(); + + if (spans.indexOf(span) === -1) + spans.push(span); } withSpan(span: Span, callback: (...args: any[]) => any, ...args: any[]): any { diff --git a/src/trace/context/DummyContext.ts b/src/trace/context/DummyContext.ts index 2d5c4fb..2fe73ab 100644 --- a/src/trace/context/DummyContext.ts +++ b/src/trace/context/DummyContext.ts @@ -23,38 +23,50 @@ import DummySpan from '../../trace/span/DummySpan'; import Segment from '../../trace/context/Segment'; import { Component } from '../../trace/Component'; import { ContextCarrier } from './ContextCarrier'; +import ContextManager from './ContextManager'; export default class DummyContext implements Context { - span: Span = DummySpan.create(this); segment: Segment = new Segment(); nSpans = 0; + finished = false; newEntrySpan(operation: string, carrier?: ContextCarrier, inherit?: Component): Span { - return this.span; + return DummySpan.create(this); } - newExitSpan(operation: string, peer: string, component: Component, inherit?: Component): Span { - return this.span; + newExitSpan(operation: string, component: Component, inherit?: Component): Span { + return DummySpan.create(this); } newLocalSpan(operation: string): Span { - return this.span; + return DummySpan.create(this); } - start(): Context { - this.nSpans++; + start(span: Span): Context { + const spans = ContextManager.spansDup(); + + if (!this.nSpans++) { + if (spans.indexOf(span) === -1) + spans.push(span); + } + return this; } - stop(): boolean { - return --this.nSpans === 0; + stop(span: Span): boolean { + if (--this.nSpans) + return false; + + ContextManager.clear(span); + + return true; } async(span: Span) { - return; + ContextManager.clear(span); } resync(span: Span) { - return; + ContextManager.restore(span); } } diff --git a/src/trace/context/SpanContext.ts b/src/trace/context/SpanContext.ts index 2436bd0..7a19781 100644 --- a/src/trace/context/SpanContext.ts +++ b/src/trace/context/SpanContext.ts @@ -25,105 +25,123 @@ import Segment from '../../trace/context/Segment'; import EntrySpan from '../../trace/span/EntrySpan'; import ExitSpan from '../../trace/span/ExitSpan'; import LocalSpan from '../../trace/span/LocalSpan'; +import SegmentRef from './SegmentRef'; +import ContextManager from './ContextManager'; import { Component } from '../../trace/Component'; import { createLogger } from '../../logging'; -import { executionAsyncId } from 'async_hooks'; import { ContextCarrier } from './ContextCarrier'; -import ContextManager from './ContextManager'; import { SpanType } from '../../proto/language-agent/Tracing_pb'; import { emitter } from '../../lib/EventEmitter'; const logger = createLogger(__filename); +emitter.on('segments-sent', () => { + SpanContext.nActiveSegments = 0; // reset limiter +}); + export default class SpanContext implements Context { + static nActiveSegments = 0; // counter to allow only config.maxBufferSize active (non-dummy) segments per reporting frame spanId = 0; nSpans = 0; + finished = false; segment: Segment = new Segment(); - get parent(): Span | null { - if (ContextManager.spans.length > 0) { - return ContextManager.spans[ContextManager.spans.length - 1]; - } - return null; + ignoreCheck(operation: string, type: SpanType, carrier?: ContextCarrier): Span | undefined { + if (operation.match(config.reIgnoreOperation) || (carrier && !carrier.isValid())) + return DummySpan.create(); + + return undefined; } - get parentId(): number { - return this.parent ? this.parent.id : -1; + spanCheck(spanType: SpanType, operation: string, carrier?: ContextCarrier): [Span | null, Span?] { + let span = this.ignoreCheck(operation, SpanType.ENTRY, carrier); + + if (span) + return [span]; + + const spans = ContextManager.spans; + const parent = spans[spans.length - 1]; + + if (parent instanceof DummySpan) + return [parent]; + + return [null, parent]; } - ignoreCheck(operation: string, type: SpanType): Span | undefined { - if (operation.match(config.reIgnoreOperation)) - return DummySpan.create(); + newSpan(spanClass: typeof EntrySpan | typeof ExitSpan | typeof LocalSpan, parent: Span, operation: string): Span { + const context = !this.finished ? this : new SpanContext(); - return undefined; + const span = new spanClass({ + id: context.spanId++, + parentId: this.finished ? -1 : parent?.id ?? -1, + context: context, + operation, + }); + + if (this.finished && parent) { // segment has already been closed and sent to server, if there is a parent span then need new segment to reference + const carrier = new ContextCarrier( + parent.context.segment.relatedTraces[0], + parent.context.segment.segmentId, + parent.id, + config.serviceName, + config.serviceInstance, + parent.operation, + parent.peer, + [], + ); + + const ref = SegmentRef.fromCarrier(carrier); + + context.segment.relate(carrier.traceId!); + span.refer(ref); + } + + return span; } newEntrySpan(operation: string, carrier?: ContextCarrier, inherit?: Component): Span { - let span = this.ignoreCheck(operation, SpanType.ENTRY); + let [span, parent] = this.spanCheck(SpanType.ENTRY, operation, carrier); if (span) return span; - const spans = ContextManager.spansDup(); - const parent = spans[spans.length - 1]; - - if (logger.isDebugEnabled()) { + if (logger._isDebugEnabled) { logger.debug('Creating entry span', { - spans, parent, }); } - if (parent && parent.type === SpanType.ENTRY && inherit && inherit === parent.component) { + if (!this.finished && parent?.type === SpanType.ENTRY && inherit && inherit === parent.component) { span = parent; parent.operation = operation; } else { - span = new EntrySpan({ - id: this.spanId++, - parentId: this.parentId, - context: this, - operation, - }); + span = this.newSpan(EntrySpan, parent!, operation); - if (carrier && carrier.isValid()) { + if (carrier && carrier.isValid()) span.extract(carrier); - } } return span; } - newExitSpan(operation: string, peer: string, component: Component, inherit?: Component): Span { - let span = this.ignoreCheck(operation, SpanType.EXIT); + newExitSpan(operation: string, component: Component, inherit?: Component): Span { + let [span, parent] = this.spanCheck(SpanType.EXIT, operation); if (span) return span; - const spans = ContextManager.spansDup(); - const parent = spans[spans.length - 1]; - - if (logger.isDebugEnabled()) { + if (logger._isDebugEnabled) { logger.debug('Creating exit span', { operation, parent, - spans, - peer, }); } - if (parent && parent.type === SpanType.EXIT && component === parent.inherit) { + if (!this.finished && parent?.type === SpanType.EXIT && component === parent.inherit) span = parent; - - } else { - span = new ExitSpan({ - id: this.spanId++, - parentId: this.parentId, - context: this, - peer, - operation, - }); - } + else + span = this.newSpan(ExitSpan, parent!, operation); if (inherit) span.inherit = inherit; @@ -132,39 +150,34 @@ export default class SpanContext implements Context { } newLocalSpan(operation: string): Span { - const span = this.ignoreCheck(operation, SpanType.LOCAL); + let [span, parent] = this.spanCheck(SpanType.LOCAL, operation); if (span) return span; - ContextManager.spansDup(); - - if (logger.isDebugEnabled()) { + if (logger._isDebugEnabled) { logger.debug('Creating local span', { - parentId: this.parentId, - executionAsyncId: executionAsyncId(), + parentId: parent?.id ?? -1, }); } - return new LocalSpan({ - id: this.spanId++, - parentId: this.parentId, - context: this, - operation, - }); + return this.newSpan(LocalSpan, parent!, operation); } start(span: Span): Context { + const spans = ContextManager.spansDup(); + logger.debug(`Starting span ${span.operation}`, { span, - spans: ContextManager.spans, + spans, nSpans: this.nSpans, }); - this.nSpans += 1; - if (ContextManager.spans.every((s) => s.id !== span.id || s.context !== span.context)) { - ContextManager.spans.push(span); - } + if (!this.nSpans++) + SpanContext.nActiveSegments += 1; + + if (spans.indexOf(span) === -1) + spans.push(span); return this; } @@ -177,15 +190,13 @@ export default class SpanContext implements Context { }); span.finish(this.segment); - - const idx = ContextManager.spans.indexOf(span); - if (idx !== -1) { - ContextManager.spans.splice(idx, 1); - } + ContextManager.clear(span); if (--this.nSpans === 0) { + this.finished = true; + emitter.emit('segment-finished', this.segment); - ContextManager.clear(); + return true; } @@ -199,16 +210,7 @@ export default class SpanContext implements Context { nSpans: this.nSpans, }); - const spans = ContextManager.spansDup(); // this needed to make sure async tasks created before this call will still have this span at the top of their span list - const idx = spans.indexOf(span); - - if (idx !== -1) { - spans.splice(idx, 1); - - if (!spans.length) { // this will pass the context to child async task so it doesn't mess with other tasks here - ContextManager.clear(); - } - } + ContextManager.clear(span); } resync(span: Span) { @@ -218,10 +220,6 @@ export default class SpanContext implements Context { nSpans: this.nSpans, }); - if (!ContextManager.hasContext || !ContextManager.spans.length) { - ContextManager.restore(span.context, [span]); - } else if (ContextManager.spans.every((s) => s.id !== span.id || s.context !== span.context)) { - ContextManager.spans.push(span); - } + ContextManager.restore(span); } } diff --git a/src/trace/span/DummySpan.ts b/src/trace/span/DummySpan.ts index c2b1571..c75f84a 100644 --- a/src/trace/span/DummySpan.ts +++ b/src/trace/span/DummySpan.ts @@ -32,6 +32,28 @@ export default class DummySpan extends Span { }); } + start(): any { + if (!this.depth++) + this.context.start(this); + } + + stop(block?: any): void { + if (!--this.depth) + this.context.stop(this); + } + + async(block?: any): void { + this.context.async(this); + } + + resync(): any { + this.context.resync(this); + } + + error(error: Error, statusOverride?: number): this { + return this; + } + inject(): ContextCarrier { return new ContextCarrier(); } diff --git a/src/trace/span/ExitSpan.ts b/src/trace/span/ExitSpan.ts index a120366..1c69708 100644 --- a/src/trace/span/ExitSpan.ts +++ b/src/trace/span/ExitSpan.ts @@ -40,7 +40,7 @@ export default class ExitSpan extends Span { this.id, config.serviceName, config.serviceInstance, - ContextManager.spans[0].operation, + this.operation, this.peer, [], );