From 68eba71191d7701198b8c777658af7156c364f7d Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 16 May 2023 02:01:38 -0400 Subject: [PATCH] feat(opencensus-shim) add ShimTracer and ShimSpan implementations (#3756) Co-authored-by: Marc Pichler --- .../packages/shim-opencensus/package.json | 1 + .../packages/shim-opencensus/src/ShimSpan.ts | 188 ++++++++++++++++++ .../shim-opencensus/src/ShimTracer.ts | 162 +++++++++++++++ .../packages/shim-opencensus/src/index.ts | 2 + .../shim-opencensus/test/ShimSpan.test.ts | 128 ++++++++++++ .../shim-opencensus/test/ShimTracer.test.ts | 165 ++++++++++++++- .../test/otel-sandwich.test.ts | 88 ++++++++ .../packages/shim-opencensus/test/util.ts | 63 ++++++ .../packages/shim-opencensus/tsconfig.json | 3 + 9 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 experimental/packages/shim-opencensus/src/ShimSpan.ts create mode 100644 experimental/packages/shim-opencensus/src/ShimTracer.ts create mode 100644 experimental/packages/shim-opencensus/test/ShimSpan.test.ts create mode 100644 experimental/packages/shim-opencensus/test/otel-sandwich.test.ts create mode 100644 experimental/packages/shim-opencensus/test/util.ts diff --git a/experimental/packages/shim-opencensus/package.json b/experimental/packages/shim-opencensus/package.json index e744d9e09e..87512230d7 100644 --- a/experimental/packages/shim-opencensus/package.json +++ b/experimental/packages/shim-opencensus/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@opentelemetry/core": "1.13.0", "@opentelemetry/context-async-hooks": "1.13.0", + "@opentelemetry/sdk-trace-base": "1.13.0", "@opencensus/core": "0.1.0", "@opentelemetry/api": "1.4.1", "@types/mocha": "10.0.0", diff --git a/experimental/packages/shim-opencensus/src/ShimSpan.ts b/experimental/packages/shim-opencensus/src/ShimSpan.ts new file mode 100644 index 0000000000..498c73e5e7 --- /dev/null +++ b/experimental/packages/shim-opencensus/src/ShimSpan.ts @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 * as oc from '@opencensus/core'; +import { ShimTracer } from './ShimTracer'; +import { AttributeValue, Span, SpanStatusCode, diag } from '@opentelemetry/api'; +import { mapMessageEvent, reverseMapSpanContext } from './transform'; + +// Copied from +// https://github.com/census-instrumentation/opencensus-node/blob/v0.1.0/packages/opencensus-core/src/trace/model/span.ts#L61 +export const DEFAULT_SPAN_NAME = 'span'; + +const STATUS_OK = { + code: oc.CanonicalCode.OK, +}; + +interface Options { + shimTracer: ShimTracer; + otelSpan: Span; + isRootSpan?: boolean | undefined; + kind?: oc.SpanKind | undefined; + parentSpanId?: string | undefined; +} + +export class ShimSpan implements oc.Span { + get id(): string { + return this.otelSpan.spanContext().spanId; + } + + get tracer(): oc.TracerBase { + return this._shimTracer; + } + + logger: oc.Logger = diag; + + /** These are not readable in OTel so we return empty values */ + attributes: oc.Attributes = {}; + annotations: oc.Annotation[] = []; + messageEvents: oc.MessageEvent[] = []; + spans: oc.Span[] = []; + links: oc.Link[] = []; + name = ''; + status: oc.Status = STATUS_OK; + activeTraceParams: oc.TraceParams = {}; + droppedAttributesCount = 0; + droppedLinksCount = 0; + droppedAnnotationsCount = 0; + droppedMessageEventsCount = 0; + started = true; + ended = false; + numberOfChildren = 0; + duration = 0; + + /** Actual private attributes */ + private _shimTracer: ShimTracer; + readonly otelSpan: Span; + private _isRootSpan: boolean; + + readonly kind: oc.SpanKind; + readonly parentSpanId: string; + + get remoteParent(): boolean { + return this.otelSpan.spanContext().isRemote ?? false; + } + + /** Constructs a new SpanBaseModel instance. */ + constructor({ + shimTracer, + otelSpan, + isRootSpan = false, + kind = oc.SpanKind.UNSPECIFIED, + parentSpanId = '', + }: Options) { + this._shimTracer = shimTracer; + this.otelSpan = otelSpan; + this._isRootSpan = isRootSpan; + this.kind = kind; + this.parentSpanId = parentSpanId; + } + + /** Returns whether a span is root or not. */ + isRootSpan(): boolean { + return this._isRootSpan; + } + + get traceId(): string { + return this.otelSpan.spanContext().traceId; + } + + /** Gets the trace state */ + get traceState(): oc.TraceState | undefined { + return this.otelSpan.spanContext().traceState?.serialize(); + } + + /** No-op implementation of this method. */ + get startTime(): Date { + return new Date(); + } + + /** No-op implementation of this method. */ + get endTime(): Date { + return new Date(); + } + + /** No-op implementation of this method. */ + allDescendants(): oc.Span[] { + return []; + } + + /** Gives the TraceContext of the span. */ + get spanContext(): oc.SpanContext { + return reverseMapSpanContext(this.otelSpan.spanContext()); + } + + addAttribute(key: string, value: string | number | boolean | object) { + this.otelSpan.setAttribute(key, value as AttributeValue); + } + + addAnnotation( + description: string, + attributes?: oc.Attributes, + timestamp?: number + ) { + this.otelSpan.addEvent(description, attributes, timestamp); + } + + /** No-op implementation of this method. */ + addLink() { + diag.info( + 'Call to OpenCensus Span.addLink() is being ignored. OTel does not support ' + + 'adding links after span creation' + ); + } + + /** No-op implementation of this method. */ + addMessageEvent( + type: oc.MessageEventType, + id: number, + timestamp?: number, + uncompressedSize?: number, + compressedSize?: number + ) { + this.otelSpan.addEvent( + ...mapMessageEvent(type, id, timestamp, uncompressedSize, compressedSize) + ); + } + + /** No-op implementation of this method. */ + setStatus(code: oc.CanonicalCode, message?: string) { + this.otelSpan.setStatus({ + code: + code === oc.CanonicalCode.OK ? SpanStatusCode.OK : SpanStatusCode.ERROR, + message, + }); + } + + /** No-op implementation of this method. */ + start() {} + + end(): void { + this.otelSpan.end(); + } + + /** No-op implementation of this method. */ + truncate() {} + + /** Starts a new Span instance as a child of this instance */ + startChildSpan(options?: oc.SpanOptions): oc.Span { + return this._shimTracer.startChildSpan({ + name: DEFAULT_SPAN_NAME, + childOf: this, + ...options, + }); + } +} diff --git a/experimental/packages/shim-opencensus/src/ShimTracer.ts b/experimental/packages/shim-opencensus/src/ShimTracer.ts new file mode 100644 index 0000000000..f5e8164e3c --- /dev/null +++ b/experimental/packages/shim-opencensus/src/ShimTracer.ts @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 * as oc from '@opencensus/core'; + +import { + Context, + context, + createContextKey, + diag, + INVALID_SPAN_CONTEXT, + trace, + Tracer, +} from '@opentelemetry/api'; +import { DEFAULT_SPAN_NAME, ShimSpan } from './ShimSpan'; +import { mapSpanContext, mapSpanKind } from './transform'; +import { shimPropagation } from './propagation'; + +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const INVALID_SPAN = trace.getSpan( + trace.setSpanContext(context.active(), INVALID_SPAN_CONTEXT) +)!; +const ROOTSPAN_KEY = createContextKey('rootspan_for_oc_shim'); + +function setRootSpan(ctx: Context, span: ShimSpan): Context { + return ctx.setValue(ROOTSPAN_KEY, span); +} + +export function getRootSpan(ctx: Context): ShimSpan | null { + return ctx.getValue(ROOTSPAN_KEY) as ShimSpan | null; +} + +export class ShimTracer implements oc.Tracer { + logger: oc.Logger = diag; + active: boolean = false; + + /** Noop implementations */ + sampler: oc.Sampler = new oc.AlwaysSampler(); + activeTraceParams: oc.TraceParams = {}; + eventListeners: oc.SpanEventListener[] = []; + // Uses the global OpenTelemetry propagator by default + propagation: oc.Propagation = shimPropagation; + + constructor(private otelTracer: Tracer) {} + + start({ propagation }: oc.TracerConfig): this { + this.active = true; + // Pass a propagation here if you want the shim to use an OpenCensus propagation instance + // instead of the OpenTelemetry global propagator. + if (propagation) { + this.propagation = propagation; + } + return this; + } + + /** Noop implementations */ + stop(): this { + this.active = false; + return this; + } + registerSpanEventListener(): void {} + unregisterSpanEventListener(): void {} + clearCurrentTrace(): void {} + onStartSpan(): void {} + onEndSpan(): void {} + setCurrentRootSpan() { + // This can't be correctly overriden since OTel context does not provide a way to set + // context without a callback. Leave noop for now. + } + + /** Gets the current root span. */ + get currentRootSpan(): oc.Span { + return ( + getRootSpan(context.active()) ?? + new ShimSpan({ + shimTracer: this, + otelSpan: INVALID_SPAN, + }) + ); + } + + /** + * Starts a root span. + * @param options A TraceOptions object to start a root span. + * @param fn A callback function to run after starting a root span. + */ + startRootSpan( + { name, kind, spanContext }: oc.TraceOptions, + fn: (root: oc.Span) => T + ): T { + const parentCtx = + spanContext === undefined + ? context.active() + : trace.setSpanContext(context.active(), mapSpanContext(spanContext)); + + const otelSpan = this.otelTracer.startSpan( + name, + { kind: mapSpanKind(kind) }, + parentCtx + ); + const shimSpan = new ShimSpan({ + shimTracer: this, + otelSpan, + isRootSpan: true, + kind, + parentSpanId: trace.getSpanContext(parentCtx)?.spanId, + }); + + let ctx = trace.setSpan(parentCtx, otelSpan); + ctx = setRootSpan(ctx, shimSpan); + return context.with(ctx, () => fn(shimSpan)); + } + + startChildSpan(options?: oc.SpanOptions): oc.Span { + const { kind, name = DEFAULT_SPAN_NAME, childOf } = options ?? {}; + const rootSpan = getRootSpan(context.active()); + + let ctx = context.active(); + if (childOf) { + ctx = trace.setSpanContext(ctx, mapSpanContext(childOf.spanContext)); + } else if (rootSpan) { + ctx = trace.setSpan(ctx, rootSpan.otelSpan); + } + + const otelSpan = this.otelTracer.startSpan( + name, + { + kind: mapSpanKind(kind), + }, + ctx + ); + return new ShimSpan({ + shimTracer: this, + otelSpan, + isRootSpan: false, + kind, + parentSpanId: trace.getSpanContext(ctx)?.spanId, + }); + } + + wrap(fn: oc.Func): oc.Func { + return context.bind(context.active(), fn); + } + + wrapEmitter(emitter: NodeJS.EventEmitter): void { + // Not sure if this requires returning the modified emitter + context.bind(context.active(), emitter); + } +} diff --git a/experimental/packages/shim-opencensus/src/index.ts b/experimental/packages/shim-opencensus/src/index.ts index bcd7cadc04..ccfe42e2f8 100644 --- a/experimental/packages/shim-opencensus/src/index.ts +++ b/experimental/packages/shim-opencensus/src/index.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export { ShimTracer } from './ShimTracer'; diff --git a/experimental/packages/shim-opencensus/test/ShimSpan.test.ts b/experimental/packages/shim-opencensus/test/ShimSpan.test.ts new file mode 100644 index 0000000000..f8883cf10c --- /dev/null +++ b/experimental/packages/shim-opencensus/test/ShimSpan.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import * as oc from '@opencensus/core'; +import * as assert from 'assert'; +import { withTestTracer, setupNodeContextManager } from './util'; +import { ShimSpan } from '../src/ShimSpan'; + +async function withTestSpan( + func: (span: ShimSpan) => void | Promise +): Promise { + return await withTestTracer(tracer => + tracer.startRootSpan({ name: 'test' }, span => func(span as ShimSpan)) + ); +} + +describe('ShimSpan', () => { + setupNodeContextManager(before, after); + + describe('id', () => { + it('should return the span id of the underlying otel span', async () => { + await withTestSpan(span => { + assert.strictEqual(span.id, span.otelSpan.spanContext().spanId); + }); + }); + }); + + describe('traceId', () => { + it('should return the trace id of the underlying otel span', async () => { + await withTestSpan(span => { + assert.strictEqual(span.traceId, span.otelSpan.spanContext().traceId); + }); + }); + }); + + describe('addAttribute', () => { + it('should add attributes', async () => { + const [span] = await withTestSpan(span => { + span.addAttribute('foo', 'bar'); + span.end(); + }); + + assert.deepStrictEqual(span.attributes, { foo: 'bar' }); + }); + }); + + describe('addAnnotation', () => { + it('should add an event', async () => { + const [span] = await withTestSpan(span => { + span.addAnnotation('the annotation', { foo: 'bar' }); + span.end(); + }); + + assert.strictEqual(span.events.length, 1); + const [{ time, ...event }] = span.events; + assert.deepStrictEqual(event, { + attributes: { + foo: 'bar', + }, + droppedAttributesCount: 0, + name: 'the annotation', + }); + }); + }); + + describe('addMessageEvent', () => { + it('should add an event', async () => { + const [span] = await withTestSpan(span => { + span.addMessageEvent(oc.MessageEventType.SENT, 98, undefined, 12, 15); + span.end(); + }); + + assert.strictEqual(span.events.length, 1); + const [{ time, ...event }] = span.events; + assert.deepStrictEqual(event, { + attributes: { + 'message.event.size.compressed': 15, + 'message.event.size.uncompressed': 12, + 'message.event.type': 'SENT', + }, + droppedAttributesCount: 0, + name: '98', + }); + }); + }); + + describe('startChildSpan', () => { + it('should start a child of the current span without options', async () => { + const [childSpan, parentSpan] = await withTestSpan(span => { + span.startChildSpan().end(); + span.end(); + }); + + assert.strictEqual(childSpan.name, 'span'); + assert.deepStrictEqual( + childSpan.parentSpanId, + parentSpan.spanContext().spanId + ); + }); + + it('should start a child of the current span with options', async () => { + const [childSpan, parentSpan] = await withTestSpan(span => { + span.startChildSpan({ name: 'child' }).end(); + span.end(); + }); + + assert.strictEqual(childSpan.name, 'child'); + assert.deepStrictEqual( + childSpan.parentSpanId, + parentSpan.spanContext().spanId + ); + }); + }); +}); diff --git a/experimental/packages/shim-opencensus/test/ShimTracer.test.ts b/experimental/packages/shim-opencensus/test/ShimTracer.test.ts index 33c99a6d8a..a65a2c67be 100644 --- a/experimental/packages/shim-opencensus/test/ShimTracer.test.ts +++ b/experimental/packages/shim-opencensus/test/ShimTracer.test.ts @@ -14,10 +14,171 @@ * limitations under the License. */ +import { Tracer as Tracer } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as oc from '@opencensus/core'; +import { ShimTracer, getRootSpan } from '../src/ShimTracer'; +import { + INVALID_SPANID, + INVALID_TRACEID, + SpanKind, + context, + createContextKey, +} from '@opentelemetry/api'; +import { withTestTracer, setupNodeContextManager } from './util'; describe('ShimTracer', () => { - it('asserts true', () => { - assert.ok(true); + setupNodeContextManager(before, after); + + it('should initially be inactive', () => { + const shimTracer = new ShimTracer(sinon.createStubInstance(Tracer)); + assert(!shimTracer.active); + }); + describe('start', () => { + it('should set the tracer as active', () => { + const shimTracer = new ShimTracer(sinon.createStubInstance(Tracer)); + shimTracer.start({}); + assert(shimTracer.active); + }); + }); + describe('stop', () => { + it('should set the tracer as inactive', () => { + const shimTracer = new ShimTracer(sinon.createStubInstance(Tracer)); + shimTracer.start({}); + assert(shimTracer.active); + }); + }); + + describe('startRootSpan', () => { + it('should create an OTel span with name', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer.startRootSpan({ name: 'test' }, span => span.end()); + }); + assert.strictEqual(otelSpans[0].name, 'test'); + }); + + it('should create an OTel span with kind', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer.startRootSpan( + { name: 'test', kind: oc.SpanKind.CLIENT }, + span => span.end() + ); + }); + assert.strictEqual(otelSpans[0].kind, SpanKind.CLIENT); + }); + + it('should create an OTel span with parent from provided spanContext', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer.startRootSpan( + { + name: 'test', + spanContext: { + traceId: '9e7ecdc193765065fee1efe757fdd874', + spanId: '4bf6239d37d8b0f0', + }, + }, + span => span.end() + ); + }); + assert.strictEqual( + otelSpans[0].spanContext().traceId, + '9e7ecdc193765065fee1efe757fdd874' + ); + assert.strictEqual(otelSpans[0].parentSpanId, '4bf6239d37d8b0f0'); + }); + + it('should set the span as root span in context', async () => { + await withTestTracer(shimTracer => { + shimTracer.startRootSpan({ name: 'test' }, span => { + assert.strictEqual(getRootSpan(context.active()), span); + }); + }); + }); + }); + + describe('currentRootSpan', () => { + it('should return an span with invalid span context if there is no root', async () => { + await withTestTracer(shimTracer => { + assert.strictEqual(shimTracer.currentRootSpan.traceId, INVALID_TRACEID); + assert.strictEqual(shimTracer.currentRootSpan.id, INVALID_SPANID); + }); + }); + + it('should return the current root span', async () => { + await withTestTracer(shimTracer => { + shimTracer.startRootSpan({ name: 'test' }, span => { + assert.strictEqual(shimTracer.currentRootSpan, span); + }); + }); + }); + }); + + describe('startChildSpan', () => { + it('should create an OTel span with a default name', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer.startChildSpan().end(); + }); + assert.strictEqual(otelSpans[0].name, 'span'); + }); + + it('should create an OTel span with name', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer.startChildSpan({ name: 'test' }).end(); + }); + assert.strictEqual(otelSpans[0].name, 'test'); + }); + + it('should create an OTel span with kind', async () => { + const otelSpans = await withTestTracer(shimTracer => { + shimTracer + .startChildSpan({ name: 'test', kind: oc.SpanKind.CLIENT }) + .end(); + }); + assert.strictEqual(otelSpans[0].kind, SpanKind.CLIENT); + }); + + it('should create an OTel span with parent from childOf', async () => { + const [childSpan, parentSpan] = await withTestTracer(shimTracer => { + const parent = shimTracer.startChildSpan({ + name: 'parent', + }); + shimTracer.startChildSpan({ name: 'child', childOf: parent }).end(); + parent.end(); + }); + assert.strictEqual( + childSpan.parentSpanId, + parentSpan.spanContext().spanId + ); + }); + + it('should create an OTel span with parent from root in context', async () => { + const [childSpan, rootSpan] = await withTestTracer(shimTracer => { + shimTracer.startRootSpan( + { + name: 'parent', + }, + root => { + shimTracer.startChildSpan({ name: 'child', childOf: root }).end(); + root.end(); + } + ); + }); + assert.strictEqual(childSpan.parentSpanId, rootSpan.spanContext().spanId); + }); + }); + + describe('wrap', () => { + it('should bind the provided function to active context at time of wrapping', () => { + const shimTracer = new ShimTracer(sinon.createStubInstance(Tracer)); + const key = createContextKey('key'); + const fnToWrap = () => + assert.strictEqual(context.active().getValue(key), 'value'); + + const fn = context.with(context.active().setValue(key, 'value'), () => + shimTracer.wrap(fnToWrap) + ); + fn(); + }); }); }); diff --git a/experimental/packages/shim-opencensus/test/otel-sandwich.test.ts b/experimental/packages/shim-opencensus/test/otel-sandwich.test.ts new file mode 100644 index 0000000000..a1034bf081 --- /dev/null +++ b/experimental/packages/shim-opencensus/test/otel-sandwich.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 * as assert from 'assert'; +import { setupNodeContextManager, withTestTracer } from './util'; + +/** + * Tests the "OpenTelemetry sandwich" problem described in the spec at + * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#trace-bridge + */ +describe('OpenTelemetry sandwich', () => { + setupNodeContextManager(before, after); + + it('should maintain parent-child relationship for OTel -> OC', async () => { + const spans = await withTestTracer((shimTracer, otelTracer) => { + otelTracer.startActiveSpan('parent-otel', parentSpan => { + shimTracer.startChildSpan({ name: 'child-oc' }).end(); + parentSpan.end(); + }); + }); + + assert.strictEqual(spans.length, 2); + const [childSpan, parentSpan] = spans; + assert.strictEqual( + childSpan.spanContext().traceId, + parentSpan.spanContext().traceId + ); + assert.strictEqual(childSpan.parentSpanId, parentSpan.spanContext().spanId); + }); + + it('should maintain parent-child relationship for OC -> OTel', async () => { + const spans = await withTestTracer((shimTracer, otelTracer) => { + shimTracer.startRootSpan({ name: 'parent-oc' }, parentSpan => { + otelTracer.startSpan('child-otel').end(); + parentSpan.end(); + }); + }); + + assert.strictEqual(spans.length, 2); + const [childSpan, parentSpan] = spans; + assert.strictEqual( + childSpan.spanContext().traceId, + parentSpan.spanContext().traceId + ); + assert.strictEqual(childSpan.parentSpanId, parentSpan.spanContext().spanId); + }); + + it('should maintain structure for OTel -> OC -> OTel', async () => { + const spans = await withTestTracer((shimTracer, otelTracer) => { + otelTracer.startActiveSpan('parent-otel', parentSpan => { + shimTracer.startRootSpan({ name: 'middle-oc' }, middleSpan => { + otelTracer.startSpan('child-otel').end(); + middleSpan.end(); + }); + parentSpan.end(); + }); + }); + + assert.strictEqual(spans.length, 3); + const [childSpan, middleSpan, parentSpan] = spans; + assert.strictEqual( + childSpan.spanContext().traceId, + parentSpan.spanContext().traceId + ); + assert.strictEqual( + middleSpan.spanContext().traceId, + parentSpan.spanContext().traceId + ); + assert.strictEqual( + middleSpan.parentSpanId, + parentSpan.spanContext().spanId + ); + assert.strictEqual(childSpan.parentSpanId, middleSpan.spanContext().spanId); + }); +}); diff --git a/experimental/packages/shim-opencensus/test/util.ts b/experimental/packages/shim-opencensus/test/util.ts new file mode 100644 index 0000000000..b28baca4d2 --- /dev/null +++ b/experimental/packages/shim-opencensus/test/util.ts @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { + AlwaysOnSampler, + BasicTracerProvider, + InMemorySpanExporter, + ReadableSpan, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { ShimTracer } from '../src/ShimTracer'; +import * as semver from 'semver'; +import { + AsyncHooksContextManager, + AsyncLocalStorageContextManager, +} from '@opentelemetry/context-async-hooks'; +import { Tracer, context } from '@opentelemetry/api'; + +export async function withTestTracer( + func: (shimTracer: ShimTracer, otelTracer: Tracer) => void | Promise +): Promise { + const tracerProvider = new BasicTracerProvider({ + sampler: new AlwaysOnSampler(), + }); + const inMemExporter = new InMemorySpanExporter(); + tracerProvider.addSpanProcessor(new SimpleSpanProcessor(inMemExporter)); + + await func( + new ShimTracer(tracerProvider.getTracer('test-shim')), + tracerProvider.getTracer('test-otel') + ); + + await tracerProvider.forceFlush(); + const spans = inMemExporter.getFinishedSpans(); + await tracerProvider.shutdown(); + return spans; +} + +export function setupNodeContextManager( + before: Mocha.HookFunction, + after: Mocha.HookFunction +) { + const ContextManager = semver.gte(process.version, '14.8.0') + ? AsyncLocalStorageContextManager + : AsyncHooksContextManager; + const instance = new ContextManager(); + instance.enable(); + before(() => context.setGlobalContextManager(instance)); + after(() => context.disable()); +} diff --git a/experimental/packages/shim-opencensus/tsconfig.json b/experimental/packages/shim-opencensus/tsconfig.json index ddf3cfdeed..91cebb5ad5 100644 --- a/experimental/packages/shim-opencensus/tsconfig.json +++ b/experimental/packages/shim-opencensus/tsconfig.json @@ -17,6 +17,9 @@ }, { "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" } ] }