-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add new DocumentLoadInstrumentation (#43)
* add DocumentLoadInstrumentation with ServerTiming * add CustomIdGenerator * adjust instrumentation * add traceID member variable in IdGenerator * fix transactionIdGeneration * save transactionSpanId * use documentload-span as root-span * add patched user-interaction-instrumentation * add delay to beforeunload-event * patch xhr version 1 * patch xhr version 2 * patch fetch * add delay configuration * add startNewTransaction() * little refactor * try patchTracer() * refactor startSpan() and remove patched instrumentations * little refactor * user server-timing-span-id to create transaction-span * outsource transaction management * fix spanId generation
- Loading branch information
Showing
7 changed files
with
380 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Also see: https://github.com/signalfx/splunk-otel-js-web/blob/main/packages/web/src/SplunkDocumentLoadInstrumentation.ts | ||
import { InstrumentationConfig } from '@opentelemetry/instrumentation'; | ||
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; | ||
import * as api from '@opentelemetry/api'; | ||
import { captureTraceParentFromPerformanceEntries } from './servertiming'; | ||
import { PerformanceEntries } from '@opentelemetry/sdk-trace-web'; | ||
import { Span, Tracer } from '@opentelemetry/sdk-trace-base'; | ||
|
||
import { isTracingSuppressed } from '@opentelemetry/core/build/src/trace/suppress-tracing' | ||
import { sanitizeAttributes } from '@opentelemetry/core/build/src/common/attributes'; | ||
import { TransactionSpanManager } from './transactionSpanManager'; | ||
|
||
export interface DocumentLoadServerTimingInstrumentationConfig extends InstrumentationConfig { | ||
recordTransaction: boolean; | ||
exporterDelay: number; | ||
} | ||
|
||
/** | ||
* Patch the Tracer class to use the transaction span as root span | ||
*/ | ||
export function patchTracer() { | ||
// Overwrite startSpan in Tracer class | ||
// Copy of the original startSpan()-function with additional logic inside the function to determine the parentContext | ||
Tracer.prototype.startSpan = function ( | ||
name: string, | ||
options: api.SpanOptions = {}, | ||
context = api.context.active() | ||
) { | ||
|
||
if (isTracingSuppressed(context)) { | ||
api.diag.debug('Instrumentation suppressed, returning Noop Span'); | ||
return api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT); | ||
} | ||
|
||
let parentContext; //getParent(options, context); | ||
if(options.root) parentContext = undefined; | ||
else parentContext = api.trace.getSpanContext(context); | ||
|
||
if(!parentContext) { | ||
const transactionSpan = TransactionSpanManager.getTransactionSpan(); | ||
if(transactionSpan) | ||
parentContext = transactionSpan.spanContext(); | ||
} | ||
|
||
// Use transaction span-ID for documentLoadSpan, if existing | ||
let spanId = this._idGenerator.generateSpanId(); | ||
if(name == "documentLoad") { | ||
const transactionSpanId = TransactionSpanManager.getTransactionSpanId(); | ||
if(transactionSpanId) spanId = transactionSpanId; | ||
} | ||
|
||
let traceId; | ||
let traceState; | ||
let parentSpanId; | ||
if (!parentContext || !api.trace.isSpanContextValid(parentContext)) { | ||
// New root span. | ||
traceId = this._idGenerator.generateTraceId(); | ||
} else { | ||
// New child span. | ||
traceId = parentContext.traceId; | ||
traceState = parentContext.traceState; | ||
parentSpanId = parentContext.spanId; | ||
} | ||
|
||
const spanKind = options.kind ?? api.SpanKind.INTERNAL; | ||
const links = options.links ?? []; | ||
const attributes = sanitizeAttributes(options.attributes); | ||
// make sampling decision | ||
const samplingResult = this._sampler.shouldSample( | ||
options.root | ||
? api.trace.setSpanContext(context, api.INVALID_SPAN_CONTEXT) | ||
: context, | ||
traceId, | ||
name, | ||
spanKind, | ||
attributes, | ||
links | ||
); | ||
|
||
const traceFlags = | ||
samplingResult.decision === api.SamplingDecision.RECORD_AND_SAMPLED | ||
? api.TraceFlags.SAMPLED | ||
: api.TraceFlags.NONE; | ||
const spanContext = { traceId, spanId, traceFlags, traceState }; | ||
if (samplingResult.decision === api.SamplingDecision.NOT_RECORD) { | ||
api.diag.debug('Recording is off, propagating context in a non-recording span'); | ||
return api.trace.wrapSpanContext(spanContext); | ||
} | ||
|
||
const span = new Span( | ||
this, | ||
context, | ||
name, | ||
spanContext, | ||
spanKind, | ||
parentSpanId, | ||
links, | ||
options.startTime | ||
); | ||
// Set default attributes | ||
span.setAttributes(Object.assign(attributes, samplingResult.attributes)); | ||
return span; | ||
} | ||
} | ||
|
||
type PerformanceEntriesWithServerTiming = PerformanceEntries & {serverTiming?: ReadonlyArray<({name: string, duration: number, description: string})>} | ||
type ExposedDocumentLoadSuper = { | ||
_startSpan(spanName: string, performanceName: string, entries: PerformanceEntries, parentSpan?: Span): api.Span | undefined; | ||
_endSpan(span: api.Span | undefined, performanceName: string, entries: PerformanceEntries): void; | ||
} | ||
|
||
export class DocumentLoadServerTimingInstrumentation extends DocumentLoadInstrumentation { | ||
readonly component: string = 'document-load-server-timing'; | ||
moduleName = this.component; | ||
|
||
constructor(config: DocumentLoadServerTimingInstrumentationConfig) { | ||
super(config); | ||
const exposedSuper = this as any as ExposedDocumentLoadSuper; | ||
const _superStartSpan: ExposedDocumentLoadSuper['_startSpan'] = exposedSuper._startSpan.bind(this); | ||
const _superEndSpan: ExposedDocumentLoadSuper['_endSpan'] = exposedSuper._endSpan.bind(this); | ||
|
||
exposedSuper._startSpan = (spanName, performanceName, entries, parentSpan) => { | ||
if (!(entries as PerformanceEntriesWithServerTiming).serverTiming && performance.getEntriesByType) { | ||
const navEntries = performance.getEntriesByType('navigation'); | ||
// @ts-ignore | ||
if (navEntries[0]?.serverTiming) { | ||
// @ts-ignore | ||
(entries as PerformanceEntriesWithServerTiming).serverTiming = navEntries[0].serverTiming; | ||
} | ||
} | ||
|
||
captureTraceParentFromPerformanceEntries(entries); | ||
const span = _superStartSpan(spanName, performanceName, entries, parentSpan); | ||
const exposedSpan = span as any as Span; | ||
if(exposedSpan.name == "documentLoad") TransactionSpanManager.setTransactionSpan(span); | ||
|
||
return span; | ||
} | ||
|
||
exposedSuper._endSpan = (span, performanceName, entries) => { | ||
|
||
const transactionSpan = TransactionSpanManager.getTransactionSpan(); | ||
// Don't close transactionSpan | ||
// transactionSpan will be closed through "beforeunload"-event | ||
if(transactionSpan && transactionSpan == span) return; | ||
|
||
return _superEndSpan(span, performanceName, entries); | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Also see: https://github.com/signalfx/splunk-otel-js-web/blob/main/packages/web/src/servertiming.ts | ||
import { PerformanceEntries } from '@opentelemetry/sdk-trace-web'; | ||
import { TransactionSpanManager } from './transactionSpanManager'; | ||
|
||
function setTransactionIds(match: RegExpMatchArray): void { | ||
if (match && match[1] && match[2]) { | ||
const traceId = match[1]; | ||
const spanId = match[2]; | ||
TransactionSpanManager.setTransactionTraceId(traceId); | ||
TransactionSpanManager.setTransactionSpanId(spanId); | ||
} | ||
} | ||
|
||
const ValueRegex = new RegExp('00-([0-9a-f]{32})-([0-9a-f]{16})-01'); | ||
|
||
export function captureTraceParentFromPerformanceEntries(entries: PerformanceEntries): void { | ||
if (!(entries as any).serverTiming) { | ||
return; | ||
} | ||
for(const st of (entries as any).serverTiming) { | ||
if (st.name === 'traceparent' && st.description) { | ||
const match = st.description.match(ValueRegex); | ||
setTransactionIds(match); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { IdGenerator } from '@opentelemetry/core'; | ||
import { TransactionSpanManager } from './transactionSpanManager'; | ||
|
||
const SPAN_ID_BYTES = 8; | ||
const TRACE_ID_BYTES = 16; | ||
const SHARED_BUFFER = Buffer.allocUnsafe(TRACE_ID_BYTES); | ||
|
||
// Copy of RandomIdGenerator (@opentelemetry/core) with additional getTransactionTraceId()-function | ||
export class CustomIdGenerator implements IdGenerator { | ||
|
||
/** | ||
* Returns a random 16-byte trace ID formatted/encoded as a 32 lowercase hex | ||
* characters corresponding to 128 bits. | ||
*/ | ||
get generateTraceId(): () => string { | ||
return this.getTransactionTraceId(); | ||
} | ||
|
||
/** | ||
* Returns a random 8-byte span ID formatted/encoded as a 16 lowercase hex | ||
* characters corresponding to 64 bits. | ||
*/ | ||
get generateSpanId(): () => string { | ||
return this.getIdGenerator(SPAN_ID_BYTES); | ||
} | ||
|
||
/** | ||
* If there is a transaction-trace-id, use it | ||
* Otherwise, generate a new trace-id the ordinary way | ||
*/ | ||
getTransactionTraceId(): () => string { | ||
const transactionTraceId = TransactionSpanManager.getTransactionTraceId(); | ||
// Use current transaction trace ID, if existing | ||
if(transactionTraceId) return () => transactionTraceId; | ||
else return this.getIdGenerator(TRACE_ID_BYTES); | ||
} | ||
|
||
getIdGenerator(bytes: number): () => string { | ||
return function generateId() { | ||
for (let i = 0; i < bytes / 4; i++) { | ||
// unsigned right shift drops decimal part of the number | ||
// it is required because if a number between 2**32 and 2**32 - 1 is generated, an out of range error is thrown by writeUInt32BE | ||
SHARED_BUFFER.writeUInt32BE((Math.random() * 2 ** 32) >>> 0, i * 4); | ||
} | ||
|
||
// If buffer is all 0, set the last byte to 1 to guarantee a valid w3c id is generated | ||
for (let i = 0; i < bytes; i++) { | ||
if (SHARED_BUFFER[i] > 0) { | ||
break; | ||
} else if (i === bytes - 1) { | ||
SHARED_BUFFER[bytes - 1] = 1; | ||
} | ||
} | ||
|
||
return SHARED_BUFFER.toString('hex', 0, bytes); | ||
}; | ||
} | ||
} |
Oops, something went wrong.