From 63736d7ba50102e33abe3fce4bf397b10fc51963 Mon Sep 17 00:00:00 2001 From: Yongseok Date: Mon, 11 Nov 2024 22:47:14 +0900 Subject: [PATCH] [#231] Support ChildTrace --- lib/context/async-id.js | 2 +- lib/context/remote-trace-root-builder.js | 26 +++- lib/context/span-builder.js | 8 ++ lib/context/span-id.js | 18 ++- lib/context/trace-context.js | 16 ++- lib/context/trace-root-builder.js | 13 +- lib/context/trace/async-span-chunk-builder.js | 6 +- lib/context/trace/call-stack.js | 49 ++++++++ lib/context/trace/child-trace-builder.js | 45 ++++++- lib/context/trace/span-event-builder.js | 113 +++++++++++++++--- lib/context/trace/span-event-recorder2.js | 58 ++++++--- lib/context/trace/span-repository.js | 5 +- lib/context/trace/stack-id.js | 37 ++++++ lib/context/trace/trace-id-builder.js | 68 ++++++++++- lib/context/trace/trace-root-span-recorder.js | 23 ++++ lib/context/trace/trace2.js | 43 +++---- lib/data/dto/agent-info.js | 10 +- lib/instrumentation/http-shared.js | 54 ++++++++- .../http/http-outgoing-request-header.js | 84 +++++++++++++ .../http/http-request-trace-builder.js | 2 +- lib/instrumentation/http/pinpoint-header.js | 21 ++++ .../http/trace-header-builder.js | 13 +- lib/instrumentation/interceptor-runner.js | 16 +-- test/context/callstack.test.js | 2 +- test/support/data-sender-mock.js | 5 + 25 files changed, 631 insertions(+), 106 deletions(-) create mode 100644 lib/context/trace/call-stack.js create mode 100644 lib/context/trace/stack-id.js create mode 100644 lib/context/trace/trace-root-span-recorder.js create mode 100644 lib/instrumentation/http/http-outgoing-request-header.js create mode 100644 lib/instrumentation/http/pinpoint-header.js diff --git a/lib/context/async-id.js b/lib/context/async-id.js index af88ebf1..20e3df42 100644 --- a/lib/context/async-id.js +++ b/lib/context/async-id.js @@ -15,7 +15,7 @@ class AsyncId { // DefaultAsyncIdGenerator.java: nextAsyncId() static make() { - return new AsyncId(AsyncId.asyncIdGenerator.next, 0) + return new AsyncId(AsyncId.asyncIdGenerator.getAndIncrement(), 0) } constructor(asyncId, sequence) { diff --git a/lib/context/remote-trace-root-builder.js b/lib/context/remote-trace-root-builder.js index a7196c98..1f535b4f 100644 --- a/lib/context/remote-trace-root-builder.js +++ b/lib/context/remote-trace-root-builder.js @@ -22,11 +22,20 @@ class RemoteTraceRoot { isSampled() { return true } + + getTraceId() { + return this.traceId + } } class RemoteTraceRootBuilder { - constructor(agentInfo) { + constructor(agentInfo, transactionId) { this.agentInfo = agentInfo + this.transactionId = transactionId + } + + make(transactionId) { + return new RemoteTraceRootBuilder(this.agentInfo, transactionId) } setTraceId(traceId) { @@ -34,13 +43,18 @@ class RemoteTraceRootBuilder { return this } - build(transactionId) { - if (this.traceId) { - return new RemoteTraceRoot(this.traceId, new TraceRootBuilder(this.agentInfo.agentId).build(transactionId)) + build() { + const agentId = this.agentInfo.getAgentId() + if (this.isNewTraceRoot()) { + const traceId = new TraceIdBuilder(this.agentInfo, this.transactionId).build() + return new RemoteTraceRoot(traceId, new TraceRootBuilder(agentId, this.transactionId).build()) } - const traceId = new TraceIdBuilder(this.agentInfo, transactionId).build() - return new RemoteTraceRoot(traceId, new TraceRootBuilder(this.agentInfo.agentId).build(transactionId)) + return new RemoteTraceRoot(this.traceId, new TraceRootBuilder(agentId, this.transactionId).build()) + } + + isNewTraceRoot() { + return !this.traceId } } diff --git a/lib/context/span-builder.js b/lib/context/span-builder.js index 77b2f844..fa72778d 100644 --- a/lib/context/span-builder.js +++ b/lib/context/span-builder.js @@ -10,6 +10,10 @@ class Span { constructor(traceRoot) { this.traceRoot = traceRoot } + + isSpan() { + return true + } } class SpanBuilder { @@ -64,6 +68,10 @@ class SpanBuilder { return this } + getTraceRoot() { + return this.traceRoot + } + build() { const span = new Span(this.traceRoot) this.startTime = this.startTime || Date.now() diff --git a/lib/context/span-id.js b/lib/context/span-id.js index bdb85e92..f3ccf4c3 100644 --- a/lib/context/span-id.js +++ b/lib/context/span-id.js @@ -8,19 +8,29 @@ // SpanId.java in Java agent class SpanId { - static nullSpanId = -1 - - constructor () { + constructor() { this.MAX_NUM = Number.MAX_SAFE_INTEGER } - get next () { + nullSpanId() { + return '-1' + } + + get next() { return Math.floor(Math.random() * this.MAX_NUM) } newSpanId() { return this.next.toString() } + + nextSpanId(spanId, parentSpanId) { + let newId = this.newSpanId() + while (newId === spanId || newId === parentSpanId) { + newId = this.newSpanId() + } + return newId + } } module.exports = new SpanId() diff --git a/lib/context/trace-context.js b/lib/context/trace-context.js index b1bb6d93..7dcd24d6 100644 --- a/lib/context/trace-context.js +++ b/lib/context/trace-context.js @@ -20,6 +20,8 @@ const SpanBuilder = require('./span-builder') const SpanChunkBuilder = require('./span-chunk-builder') const SpanRepository = require('./trace/span-repository') const Trace2 = require('./trace/trace2') +const AsyncSpanChunkBuilder = require('./trace/async-span-chunk-builder') +const ChildTraceBuilder = require('./trace/child-trace-builder') class TraceContext { constructor(agentInfo, dataSender, config) { @@ -33,6 +35,10 @@ class TraceContext { this.traceSampler = new TraceSampler(agentInfo, config) } + getAgentInfo() { + return this.agentInfo + } + continueTraceObject(requestData) { const traceId = new TraceId( requestData.transactionId, @@ -125,14 +131,16 @@ class TraceContext { const spanBuilder = new SpanBuilder(traceRoot) const spanChunkBuilder = new SpanChunkBuilder(traceRoot) const repository = new SpanRepository(spanChunkBuilder, this.dataSender) - return new Trace2(spanBuilder, repository, this.agentInfo.getServiceType()) + return new Trace2(spanBuilder, repository) } // DefaultAsyncContext.java: newAsyncContextTrace // DefaultBaseTraceFactory.java: continueAsyncContextTraceObject - continueAsyncContextTraceObject(traceRoot, asyncId) { - const localAsyncId = asyncId.nextLocalAsyncId2() - + // AsyncContextSpanEventEndPointApiAwareInterceptor.java : before + continueAsyncContextTraceObject(traceRoot, localAsyncId) { + const spanChunkBuilder = new AsyncSpanChunkBuilder(traceRoot, localAsyncId) + const repository = new SpanRepository(spanChunkBuilder, this.dataSender) + return new ChildTraceBuilder(traceRoot, repository, localAsyncId) } } diff --git a/lib/context/trace-root-builder.js b/lib/context/trace-root-builder.js index 046d6d50..394a9a39 100644 --- a/lib/context/trace-root-builder.js +++ b/lib/context/trace-root-builder.js @@ -23,12 +23,19 @@ class TraceRoot { } class TraceRootBuilder { - constructor(agentId) { + constructor(agentId, localTransactionId) { this.agentId = agentId + this.localTransactionId = localTransactionId + this.traceStartTime = Date.now() } - build(transactionId) { - return new TraceRoot(this.agentId, Date.now(), transactionId) + make(localTransactionId) { + return new TraceRootBuilder(this.agentId, localTransactionId) + } + + // DefaultTraceRootFactory.java: newDisableTraceRoot + build() { + return new TraceRoot(this.agentId, this.traceStartTime, this.localTransactionId) } } diff --git a/lib/context/trace/async-span-chunk-builder.js b/lib/context/trace/async-span-chunk-builder.js index 2afa451b..7fea4d79 100644 --- a/lib/context/trace/async-span-chunk-builder.js +++ b/lib/context/trace/async-span-chunk-builder.js @@ -19,6 +19,10 @@ class AsyncSpanChunk extends SpanChunk { return this.localAsyncId } + isAsyncSpanChunk() { + return true + } + toString() { return `AsyncSpanChunk(traceRoot=${this.traceRoot}, spanEventList=${this.spanEventList}, localAsyncId=${this.localAsyncId})` } @@ -32,7 +36,7 @@ class AsyncSpanChunkBuilder { } build(spanEventList) { - return new AsyncSpanChunk(this.traceRoot, this.asyncId, spanEventList) + return new AsyncSpanChunk(this.traceRoot, spanEventList, this.asyncId) } toString() { diff --git a/lib/context/trace/call-stack.js b/lib/context/trace/call-stack.js new file mode 100644 index 00000000..63169d67 --- /dev/null +++ b/lib/context/trace/call-stack.js @@ -0,0 +1,49 @@ +/** + * Pinpoint Node.js Agent + * Copyright 2020-present NAVER Corp. + * Apache License v2.0 + */ + +'use strict' + +const SpanEventBuilder = require('./span-event-builder') +const SpanEventRecorder = require('./span-event-recorder2') + +/** + * DefaultCallStack.java in Java agent + */ +class CallStack { + constructor() { + this.stack = [] + this.sequence = 0 + this.depth = 0 + } + + makeSpanEventRecorder(stackId) { + const recorder = new SpanEventRecorder(SpanEventBuilder.make(stackId)) + this.push(recorder.getSpanEventBuilder()) + return recorder + } + + push(spanEventBuilder) { + if (spanEventBuilder.needsSequence()) { + spanEventBuilder.setSequence(this.sequence++) + } + + if (spanEventBuilder.needsDepth()) { + spanEventBuilder.setDepth(this.stack.length + 1) + } + + this.stack.push(spanEventBuilder) + } + + // pop in java agent + pop() { + if (this.stack.length < 1) { + return SpanEventBuilder.nullObject() + } + return this.stack.pop() + } +} + +module.exports = CallStack \ No newline at end of file diff --git a/lib/context/trace/child-trace-builder.js b/lib/context/trace/child-trace-builder.js index 55ef4bf4..1b58cb3b 100644 --- a/lib/context/trace/child-trace-builder.js +++ b/lib/context/trace/child-trace-builder.js @@ -6,8 +6,11 @@ 'use strict' -const SpanEventRecorder = require('./span-event-recorder') +const SpanEventRecorder = require('./span-event-recorder2') const Trace = require('./trace2') +const TraceRootSpanRecorder = require('./trace-root-span-recorder') +const StackId = require('./stack-id') +const CallStack = require('./call-stack') class ChildTrace extends Trace { constructor(spanBuilder, repository, localAsyncId) { @@ -17,14 +20,48 @@ class ChildTrace extends Trace { } class ChildTraceBuilder { - constructor(traceRoot, localAsyncId) { + constructor(traceRoot, repository, localAsyncId) { this.traceRoot = traceRoot + this.repository = repository + this.spanRecorder = new TraceRootSpanRecorder(traceRoot) this.localAsyncId = localAsyncId - this.callStack = [] + this.callStack = new CallStack() this.closed = false - this.spanEventRecorder = SpanEventRecorder.nullObject() + this.traceBlockBegin(StackId.asyncBeginStackId) + } + + traceBlockBegin(stackId = StackId.default) { + if (this.closed) { + return SpanEventRecorder.nullObject() + } + + return this.callStack.makeSpanEventRecorder(stackId) + } + + traceBlockEnd() { + if (this.closed) { + return + } + + const spanEventBuilder = this.callStack.pop() + spanEventBuilder.markAfterTime() + this.repository.storeSpanEvent(spanEventBuilder) + } + + getTraceRoot() { + return this.traceRoot + } + + getTraceId() { + return this.traceRoot.getTraceId() + } + + close() { + this.traceBlockEnd(StackId.asyncBeginStackId) + this.closed = true + this.repository.flush() } } diff --git a/lib/context/trace/span-event-builder.js b/lib/context/trace/span-event-builder.js index dc70a5f4..1e0dd346 100644 --- a/lib/context/trace/span-event-builder.js +++ b/lib/context/trace/span-event-builder.js @@ -6,27 +6,78 @@ 'use strict' +const StackId = require('./stack-id') +const AsyncId = require('../async-id') + class SpanEvent { - constructor(sequence) { + static nullObject = new SpanEvent(0, 0) + + constructor(sequence, depth) { this.sequence = sequence + this.depth = depth } } + class SpanEventBuilder { // DefaultCallStack.java: push sequence 0 start value, depth 1 start value static nullObject() { - return new SpanEventBuilder(-1, 0) + return new SpanEventBuilder(StackId.nullObject) } - constructor(sequence, depth) { - this.sequence = sequence - this.depth = depth + static default() { + return new SpanEventBuilder(StackId.default) + } + + static root() { + return new SpanEventBuilder(StackId.root) + } + + static make(stackId) { + if (stackId === StackId.nullObject) { + return SpanEventBuilder.nullObject() + } + + if (stackId === StackId.root) { + return SpanEventBuilder.root() + } + + if (stackId === StackId.default) { + return SpanEventBuilder.default() + } + + return new SpanEventBuilder(stackId) + } + + constructor(stackId) { + this.stackId = stackId this.annotations = [] this.startTime = Date.now() + this.apiId = 0 + this.depth = -1 + this.nextSpanId = '-1' + } + + needsSequence() { + return this.stackId !== StackId.nullObject } - markElapsedTime() { - this.elapsedTime = Date.now() - this.startTime + needsDepth() { + return this.stackId !== StackId.nullObject + } + + setSequence(sequence) { + this.sequence = sequence + return this + } + + setDepth(depth) { + this.depth = depth + return this + } + + setServiceType(serviceType) { + this.serviceType = serviceType return this } @@ -40,23 +91,57 @@ class SpanEventBuilder { return this } + // WrappedSpanEventRecorder.java: getNextAsyncId + getAsyncId() { + if (!this.asyncId) { + this.asyncId = AsyncId.make() + } + return this.asyncId + } + setAsyncId(asyncId) { this.asyncId = asyncId return this } - makeSequenceAndDepthGrowth() { - return new SpanEventBuilder(this.sequence + 1, this.depth + 1) + markAfterTime() { + this.setAfterTime(Date.now()) + return this + } + + setAfterTime(afterTime) { + this.elapsedTime = afterTime - this.startTime + return this + } + + setNextSpanId(nextSpanId) { + this.nextSpanId = nextSpanId + return this + } + + setDestinationId(destinationId) { + this.destinationId = destinationId + return this } build() { - const spanEvent = new SpanEvent(this.sequence) - spanEvent.apiId = this.apiId - spanEvent.depth = this.depth - spanEvent.annotations = this.annotations + if (this.stackId === nullObjectStackId) { + return SpanEvent.nullObject + } + + if (!this.elapsedTime) { + this.markAfterTime() + } + + const spanEvent = new SpanEvent(this.sequence, this.depth) spanEvent.startTime = this.startTime - spanEvent.elapsedTime = this.elapsedTime + spanEvent.serviceType = this.serviceType + spanEvent.annotations = this.annotations + spanEvent.apiId = this.apiId spanEvent.asyncId = this.asyncId + spanEvent.elapsedTime = this.elapsedTime + spanEvent.nextSpanId = this.nextSpanId + spanEvent.destinationId = this.destinationId return spanEvent } } diff --git a/lib/context/trace/span-event-recorder2.js b/lib/context/trace/span-event-recorder2.js index d30ab6d9..4699ecc4 100644 --- a/lib/context/trace/span-event-recorder2.js +++ b/lib/context/trace/span-event-recorder2.js @@ -9,7 +9,8 @@ const annotationKey = require('../../constant/annotation-key') const Annotations = require('../../instrumentation/context/annotation/annotations') const SpanEventBuilder = require('./span-event-builder') -const AsyncId = require('../../context/async-id') +const log = require('../../utils/logger') +const AnnotationKeyUtils = require('../annotation-key-utils') class SpanEventRecorder { static nullObject() { @@ -20,16 +21,12 @@ class SpanEventRecorder { this.spanEventBuilder = spanEventBuilder } - makeSequenceAndDepthGrowth() { - return new SpanEventRecorder(this.spanEventBuilder.makeSequenceAndDepthGrowth()) - } - getSpanEventBuilder() { return this.spanEventBuilder } recordServiceType(serviceType) { - this.spanEventBuilder.addAnnotation(serviceType) + this.spanEventBuilder.setServiceType(serviceType.getCode()) } recordApiDesc(desc) { @@ -53,18 +50,49 @@ class SpanEventRecorder { } recordNextAsyncId() { - this.asyncId = this.asyncId.sequenceNextLocalAsyncId() - this.spanEventBuilder.setNextAsyncId(this.asyncId.getAsyncId()) + return this.spanEventBuilder.getAsyncId() + } + + recordNextSpanId(nextSpanId) { + if (nextSpanId == '-1') { + return + } + this.spanEventBuilder.setNextSpanId(nextSpanId) + } + + recordDestinationId(destinationId) { + this.spanEventBuilder.setDestinationId(destinationId) + } + + recordApi(methodDescriptor, args) { + if (typeof methodDescriptor?.getApiId !== 'function' + || typeof methodDescriptor?.getFullName !== 'function' + || typeof methodDescriptor?.getApiDescriptor !== 'function') { + return + } + + if (methodDescriptor.getApiId() == 0) { + this.recordAttribute(annotationKey.API, methodDescriptor.getFullName()) + } + + this.spanEventBuilder.setApiId(methodDescriptor.getApiId()) + this.recordArgs(args) } - getNextAsyncId() { - const nextAsyncId = this.spanEventBuilder.getAsyncId() - if (!nextAsyncId) { - const asyncId = AsyncId.make() - this.spanEventBuilder.setAsyncId(asyncId) - return asyncId + recordArgs(args) { + if (typeof args?.length !== 'number') { + return + } + + const max = Math.min(args.length, annotationKey.MAX_ARGS_SIZE) + try { + for (let index = 0; index < max; index++) { + let value = args[index] + this.recordAttribute(AnnotationKeyUtils.getArgs(index), value) + } + } catch (error) { + log.error(`recordArgs error ${error}`) } - return nextAsyncId } } diff --git a/lib/context/trace/span-repository.js b/lib/context/trace/span-repository.js index 342a4c91..8a2164cb 100644 --- a/lib/context/trace/span-repository.js +++ b/lib/context/trace/span-repository.js @@ -20,7 +20,7 @@ class SpanRepository { if (!spanEvent) { return } - + this.buffer.push(spanEvent) if (this.isOverflow()) { @@ -45,6 +45,9 @@ class SpanRepository { } storeSpan(span) { + if (span) { + span.spanEventList = this.bufferDrain() + } this.dataSender.send(span) } diff --git a/lib/context/trace/stack-id.js b/lib/context/trace/stack-id.js new file mode 100644 index 00000000..0ceafecd --- /dev/null +++ b/lib/context/trace/stack-id.js @@ -0,0 +1,37 @@ +/** + * Pinpoint Node.js Agent + * Copyright 2020-present NAVER Corp. + * Apache License v2.0 + */ + +'use strict' + +class StackId { + /** + * A constant representing the ID for a null object stack. + * This is used to signify that the stack ID is invalid or not set. + * Only for Node.js Agent internal use. + * + * @constant {number} -2 + */ + static nullObject = -2 + + /** + * The default stack ID used in the span event builder. + * This value is set to -1 by default. + * + * @constant {number} -1 + */ + static default = -1 + + /** + * The root stack identifier. + * + * @constant {number} 0 + */ + static root = 0 + + static asyncBeginStackId = 1001 +} + +module.exports = StackId \ No newline at end of file diff --git a/lib/context/trace/trace-id-builder.js b/lib/context/trace/trace-id-builder.js index b64859a1..a6dc5369 100644 --- a/lib/context/trace/trace-id-builder.js +++ b/lib/context/trace/trace-id-builder.js @@ -6,11 +6,59 @@ 'use strict' +const spanId = require('../span-id') + +const delimiter = '^' +// DefaultTraceId.java class TraceId { - constructor(agentId, agentStartTime, transactionId) { + constructor(agentId, agentStartTime, transactionId, parentSpanId, spanId, flags) { this.agentId = agentId this.agentStartTime = agentStartTime this.transactionId = transactionId + this.parentSpanId = parentSpanId + this.spanId = spanId + this.flags = flags + } + + getAgentId() { + return this.agentId + } + + getAgentStartTime() { + return this.agentStartTime + } + + getTransactionId() { + return this.transactionId + } + + getParentSpanId() { + return this.parentSpanId + } + + getSpanId() { + return this.spanId + } + + getFlags() { + return this.flags + } + + getNextTraceId() { + return new TraceId(this.agentId, this.agentStartTime, this.transactionId, this.spanId, spanId.nextSpanId(this.spanId, this.parentSpanId), this.flags) + } + + isRoot() { + return this.parentSpanId == spanId.nullSpanId() + } + + toString() { + return `TraceId(agentId=${this.agentId}, agentStartTime=${this.agentStartTime}, transactionId=${this.transactionId}, parentSpanId=${this.parentSpanId} + , spanId=${this.spanId}, flags=${this.flags})` + } + + toStringDelimiterFormatted() { + return [this.agentId, this.agentStartTime, this.transactionId].join(delimiter) } } @@ -18,10 +66,26 @@ class TraceIdBuilder { constructor(agentInfo, transactionId) { this.agentInfo = agentInfo this.transactionId = transactionId + this.parentSpanId = spanId.nullSpanId() + } + + make(transactionId) { + return new TraceIdBuilder(this.agentInfo, transactionId) + } + + setSpanId(spanId) { + this.spanId = spanId + return this + } + + setParentSpanId(parentSpanId) { + this.parentSpanId = parentSpanId + return this } build() { - return new TraceId(this.agentInfo.agentId, this.agentInfo.agentStartTime, this.transactionId) + return new TraceId(this.agentInfo.getAgentId(), this.agentInfo.getAgentStartTime() + , this.transactionId, this.parentSpanId, this.spanId ?? spanId.newSpanId(), 0) } } diff --git a/lib/context/trace/trace-root-span-recorder.js b/lib/context/trace/trace-root-span-recorder.js new file mode 100644 index 00000000..10fa602c --- /dev/null +++ b/lib/context/trace/trace-root-span-recorder.js @@ -0,0 +1,23 @@ +/** + * Pinpoint Node.js Agent + * Copyright 2020-present NAVER Corp. + * Apache License v2.0 + */ + +'use strict' + +class TraceRootSpanRecorder { + constructor(traceRoot) { + this.traceRoot = traceRoot + } + + canSampled() { + return true + } + + isRoot() { + return this.traceRoot.getTraceId().isRoot() + } +} + +module.exports = TraceRootSpanRecorder \ No newline at end of file diff --git a/lib/context/trace/trace2.js b/lib/context/trace/trace2.js index 392516e6..e3ee610b 100644 --- a/lib/context/trace/trace2.js +++ b/lib/context/trace/trace2.js @@ -8,12 +8,13 @@ const SpanEventRecorder = require('./span-event-recorder2') const SpanRecorder = require('./span-recorder2') -const log = require('../../utils/logger') +const CallStack = require('./call-stack') +const StackId = require('./stack-id') class Trace { /** * Creates an instance of the Trace class. - * + * * @constructor * @param {SpanBuilder} spanBuilder - The builder for creating spans. * @param {Repository} repository - The repository for storing trace data. @@ -23,47 +24,32 @@ class Trace { this.repository = repository this.spanRecorder = new SpanRecorder(spanBuilder) - this.callStack = [] + this.callStack = new CallStack() this.closed = false - - this.spanEventRecorder = SpanEventRecorder.nullObject() } // DefaultTrace.java: traceBlockEnd - traceBlockBegin() { + traceBlockBegin(stackId = StackId.default) { if (this.closed) { return SpanEventRecorder.nullObject() } // GrpcSpanProcessorV2: postProcess - const spanEventRecorder = this.spanEventRecorder.makeSequenceAndDepthGrowth() - this.callStack.push(spanEventRecorder.getSpanEventBuilder()) - - return spanEventRecorder + return this.callStack.makeSpanEventRecorder(stackId) } - traceBlockEnd(spanEventRecorder) { + traceBlockEnd() { if (this.closed) { return } - const index = this.callStack.findIndex(item => item === spanEventRecorder.getSpanEventBuilder()) - if (index < 0) { - log.error('SpanEvent does not exist in call-stack', spanEventRecorder.getSpanEventBuilder()) - return - } - - if (index === this.callStack.length - 1) { - this.completeSpanEventBuilder(this.callStack.pop()) - } else { - const spanEvent = this.callStack.slice(index, 1)?.[0] - this.completeSpanEventBuilder(spanEvent) - } + const spanEventBuilder = this.callStack.pop() + spanEventBuilder.markAfterTime() + this.repository.storeSpanEvent(spanEventBuilder) } - completeSpanEventBuilder(builder) { - builder?.markElapsedTime() - this.repository.storeSpanEvent(builder?.build()) + recordServiceType(serviceType) { + this.spanRecorder.recordServiceType(serviceType) } recordApi(methodDescriptor) { @@ -90,11 +76,16 @@ class Trace { return this.spanBuilder.getTraceRoot().getTraceId() } + getTraceRoot() { + return this.spanBuilder.getTraceRoot() + } + canSampled() { return true } close() { + this.traceBlockEnd() this.closed = true this.repository.storeSpan(this.spanBuilder.build()) } diff --git a/lib/data/dto/agent-info.js b/lib/data/dto/agent-info.js index de4dd362..d0a9d966 100644 --- a/lib/data/dto/agent-info.js +++ b/lib/data/dto/agent-info.js @@ -42,10 +42,18 @@ class AgentInfo { getAgentId() { return this.agentId } - + + getAgentStartTime() { + return this.agentStartTime + } + getServiceType() { return this.serviceType } + + getApplicationName() { + return this.applicationName + } } module.exports = AgentInfo diff --git a/lib/instrumentation/http-shared.js b/lib/instrumentation/http-shared.js index 927b9849..e6d0b3dd 100644 --- a/lib/instrumentation/http-shared.js +++ b/lib/instrumentation/http-shared.js @@ -18,6 +18,7 @@ const ServiceType = require('../context/service-type') const localStorage = require('./context/local-storage') const TraceBuilder = require('./context/trace-builder') const HttpRequestTraceBuilder = require('./http/http-request-trace-builder') +const HttpOutgoingRequestHeader = require('./http/http-outgoing-request-header') let pathMatcher const getPathMatcher = () => { @@ -106,10 +107,61 @@ exports.instrumentRequest = function (agent) { } } +// HttpMethodBaseExecuteMethodInterceptor.java: before exports.traceOutgoingRequest2 = function (agent, moduleName) { return function (original) { return function () { const req = original.apply(this, arguments) + + const requestHeader = new HttpOutgoingRequestHeader(agent.traceContext, req) + requestHeader.writePinpointHeader() + + const trace = agent.traceContext.currentTraceObject() + if (!trace?.canSampled()) { + return req + } + + const spanEventRecorder = trace.traceBlockBegin() + spanEventRecorder.recordServiceType(ServiceType.asyncHttpClientInternal) + spanEventRecorder.recordApiDesc('http.request') + const asyncId = spanEventRecorder.recordNextAsyncId() + trace.traceBlockEnd(spanEventRecorder) + + // DefaultAsyncContext.java: newAsyncContextTrace + const childTraceBuilder = agent.traceContext.continueAsyncContextTraceObject(trace.getTraceRoot(), asyncId.nextLocalAsyncId2()) + const recorder = childTraceBuilder.traceBlockBegin() + recorder.recordServiceType(ServiceType.asyncHttpClient) + recorder.recordApiDesc(req.method) + recorder.recordAttribute(annotationKey.HTTP_URL, requestHeader.getHostWithPathname()) + + const nextId = childTraceBuilder.getTraceId().getNextTraceId() + recorder.recordNextSpanId(nextId.getSpanId()) + recorder.recordDestinationId(requestHeader.getHost()) + + req.on('response', function (res) { + // Inspired by: + // https://github.com/nodejs/node/blob/9623ce572a02632b7596452e079bba066db3a429/lib/events.js#L258-L274 + if (res.prependListener) { + res.prependListener('end', onEnd) + } else { + const existing = res._events && res._events.end + if (!existing) { + res.on('end', onEnd) + } else { + if (typeof existing === 'function') { + res._events.end = [onEnd, existing] + } else { + existing.unshift(onEnd) + } + } + } + }) + + function onEnd() { + recorder?.recordAttribute(annotationKey.HTTP_STATUS_CODE, this.statusCode) + childTraceBuilder?.traceBlockEnd(recorder) + childTraceBuilder?.close() + } return req } } @@ -162,7 +214,6 @@ exports.traceOutgoingRequest = function (agent, moduleName) { return req function onresponse(res) { - log.debug('intercepted http.ClientcRequest response event %o', { id: httpURL }) // Inspired by: // https://github.com/nodejs/node/blob/9623ce572a02632b7596452e079bba066db3a429/lib/events.js#L258-L274 if (res.prependListener) { @@ -182,7 +233,6 @@ exports.traceOutgoingRequest = function (agent, moduleName) { } function onEnd() { - log.debug('intercepted http.IncomingMessage end event %o', { id: httpURL }) if (asyncTrace) { asyncEventRecorder.recordAttribute(annotationKey.HTTP_STATUS_CODE, this.statusCode) asyncTrace.traceAsyncEnd(asyncEventRecorder) diff --git a/lib/instrumentation/http/http-outgoing-request-header.js b/lib/instrumentation/http/http-outgoing-request-header.js new file mode 100644 index 00000000..4ed7507d --- /dev/null +++ b/lib/instrumentation/http/http-outgoing-request-header.js @@ -0,0 +1,84 @@ +/** + * Pinpoint Node.js Agent + * Copyright 2020-present NAVER Corp. + * Apache License v2.0 + */ + +'use strict' + +const samplingFlag = require('../../sampler/sampling-flag') +const Header = require('./pinpoint-header') + +// DefaultRequestTraceWriter.java +class HttpClientRequest { + constructor(request) { + this.request = request + this.host = this.getHeader('host') + } + + // write(T header) + writeSampledHeaderFalse() { + this.setHeader(Header.sampled, samplingFlag.samplingRateFalse()) + } + + // write(T header, final TraceId traceId, final String host) + write(traceId, agentInfo) { + this.setHeader(Header.traceId, traceId.toStringDelimiterFormatted()) + this.setHeader(Header.spanId, traceId.getSpanId()) + this.setHeader(Header.parentSpanId, traceId.getParentSpanId()) + this.setHeader(Header.flags, traceId.getFlags()) + this.setHeader(Header.parentApplicationName, agentInfo.getApplicationName()) + this.setHeader(Header.parentApplicationType, agentInfo.getServiceType()) + + if (this.host) { + this.setHeader(Header.host, this.getHost()) + } + } + + setHeader(name, value) { + this.request.setHeader?.(name, value) + } + + getHeader(name) { + return this.request.getHeader?.(name) + } + + getHost() { + return this.host + } + + getHostWithPathname() { + return this.getHost() + new URL(`${this.request.protocol}//${this.getHost()}${this.request.path}`).pathname + } +} + +class HttpOutgoingRequestHeader { + constructor(context, request) { + this.traceContext = context + this.request = new HttpClientRequest(request) + } + + getHostWithPathname() { + return this.request.getHostWithPathname() + } + + getHost() { + return this.request.getHost() + } + + writePinpointHeader() { + const trace = this.traceContext.currentTraceObject() + if (!trace) { + return + } + + if (!trace.canSampled()) { + this.request.writeSampledHeaderFalse() + return + } + + this.request.write(trace.getTraceId(), this.traceContext.getAgentInfo()) + } +} + +module.exports = HttpOutgoingRequestHeader \ No newline at end of file diff --git a/lib/instrumentation/http/http-request-trace-builder.js b/lib/instrumentation/http/http-request-trace-builder.js index 4a5f5c21..753af630 100644 --- a/lib/instrumentation/http/http-request-trace-builder.js +++ b/lib/instrumentation/http/http-request-trace-builder.js @@ -71,7 +71,7 @@ class HttpRequestTraceBuilder { return new HttpRequestTrace(this.request.getUrlPathname(), () => { const trace = this.traceContext.continueTraceObject() this.record(trace) - + // ServerRequestRecorder.java: recordParentInfo trace.recordAcceptorHost(traceHeader.getHost()) trace.recordParentApplication(traceHeader.getParentApplicationName(), traceHeader.getParentApplicationType()) diff --git a/lib/instrumentation/http/pinpoint-header.js b/lib/instrumentation/http/pinpoint-header.js new file mode 100644 index 00000000..9dc6cfcd --- /dev/null +++ b/lib/instrumentation/http/pinpoint-header.js @@ -0,0 +1,21 @@ +/** + * Pinpoint Node.js Agent + * Copyright 2020-present NAVER Corp. + * Apache License v2.0 + */ + +'use strict' + +class Header { + static traceId = 'Pinpoint-TraceID' + static spanId = 'Pinpoint-SpanID' + static parentSpanId = 'Pinpoint-pSpanID' + static sampled = 'Pinpoint-Sampled' + static flags = 'Pinpoint-Flags' + static parentApplicationName = 'Pinpoint-pAppName' + static parentApplicationType = 'Pinpoint-pAppType' + static parentApplicationNamespace = 'Pinpoint-pAppNamespace' + static host = 'Pinpoint-Host' +} + +module.exports = Header \ No newline at end of file diff --git a/lib/instrumentation/http/trace-header-builder.js b/lib/instrumentation/http/trace-header-builder.js index 609dea7e..4b980f71 100644 --- a/lib/instrumentation/http/trace-header-builder.js +++ b/lib/instrumentation/http/trace-header-builder.js @@ -7,18 +7,7 @@ 'use strict' const samplingFlag = require('../../sampler/sampling-flag') - -class Header { - static traceId = 'Pinpoint-TraceID' - static spanId = 'Pinpoint-SpanID' - static parentSpanId = 'Pinpoint-pSpanID' - static sampled = 'Pinpoint-Sampled' - static flags = 'Pinpoint-Flags' - static parentApplicationName = 'Pinpoint-pAppName' - static parentApplicationType = 'Pinpoint-pAppType' - static parentApplicationNamespace = 'Pinpoint-pAppNamespace' - static host = 'Pinpoint-Host' -} +const Header = require('./pinpoint-header') class TraceHeader { static newTrace = new TraceHeader() diff --git a/lib/instrumentation/interceptor-runner.js b/lib/instrumentation/interceptor-runner.js index 37e088cd..3fd4d34a 100644 --- a/lib/instrumentation/interceptor-runner.js +++ b/lib/instrumentation/interceptor-runner.js @@ -29,24 +29,24 @@ class InterceptorRunner { } else { builder = this.interceptor.methodDescriptorBuilder } - + if (typeof wrapper.prepareBeforeTrace === 'function') { wrapper.prepareBeforeTrace() } - + const trace = localStorage.getStore() if (trace && builder.isDetectedFunctionName()) { recorder = trace.traceBlockBegin() - + if (this.interceptor.serviceType) { recorder.recordServiceType(this.interceptor.serviceType) } - + const methodDescriptor = apiMetaService.cacheApiWithBuilder(builder) if (methodDescriptor) { recorder.recordApi(methodDescriptor) } - + if (typeof wrapper.doInBeforeTrace === 'function') { wrapper.doInBeforeTrace(recorder) } @@ -58,15 +58,15 @@ class InterceptorRunner { const result = this.original.apply(thisArg, argsArray) - try { + try { if (typeof wrapper.prepareAfterTrace === 'function') { wrapper.prepareAfterTrace() } - + if (recorder && typeof wrapper.doInAfterTrace === 'function') { wrapper.doInAfterTrace(recorder, result) } - + const trace = localStorage.getStore() if (trace && recorder) { trace.traceBlockEnd(recorder) diff --git a/test/context/callstack.test.js b/test/context/callstack.test.js index 82ad5554..c7457e2b 100644 --- a/test/context/callstack.test.js +++ b/test/context/callstack.test.js @@ -23,7 +23,7 @@ test(`span and spanEvent call stack`, async (t) => { const trace = agent.createTraceObject() localStorage.run(trace, () => { - t.equal(trace.callStack.length, 0, 'callstack is 0') + t.equal(trace.callStack.length ?? trace.callStack.stack.length, 0, 'callstack is 0') t.equal(agent.traceContext.currentTraceObject(), trace, 'current trace is current asyncId trace object') axios.get(`https://github.com`, { httpAgent: new http.Agent({ keepAlive: false }) }) diff --git a/test/support/data-sender-mock.js b/test/support/data-sender-mock.js index 3a838ee4..a57a9367 100644 --- a/test/support/data-sender-mock.js +++ b/test/support/data-sender-mock.js @@ -38,6 +38,11 @@ class MockDataSender extends DataSender { this.mockSpanChunks.push(data) } else if (data instanceof SqlMetaData) { this.mockSqlMetaData = data + } else if (data?.isAsyncSpanChunk?.()) { + this.mockSpanChunks.push(data) + } else if (data?.isSpan?.()) { + this.mockSpan = data + this.mockSpans.push(data) } }