From fbe7d47fbd5ac3d9156f360d7257c7b5d6df4338 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 20 Apr 2023 21:34:22 +0000 Subject: [PATCH] feat(opencensus-shim) add mapping logic and propagation shim --- .../package.json | 13 +- .../src/propagation.ts | 78 ++++++++++ .../src/transform.ts | 105 +++++++++++++ .../test/propagation.test.ts | 85 +++++++++++ .../test/transform.test.ts | 143 ++++++++++++++++++ 5 files changed, 419 insertions(+), 5 deletions(-) create mode 100644 experimental/packages/opentelemetry-shim-opencensus/src/propagation.ts create mode 100644 experimental/packages/opentelemetry-shim-opencensus/src/transform.ts create mode 100644 experimental/packages/opentelemetry-shim-opencensus/test/propagation.test.ts create mode 100644 experimental/packages/opentelemetry-shim-opencensus/test/transform.test.ts diff --git a/experimental/packages/opentelemetry-shim-opencensus/package.json b/experimental/packages/opentelemetry-shim-opencensus/package.json index 8779173e9f..ed8b122414 100644 --- a/experimental/packages/opentelemetry-shim-opencensus/package.json +++ b/experimental/packages/opentelemetry-shim-opencensus/package.json @@ -10,6 +10,7 @@ "prepublishOnly": "npm run compile", "compile": "tsc --build", "clean": "tsc --build --clean", + "tdd": "npm run test -- --extension ts --watch", "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", "lint": "eslint . --ext .ts", @@ -44,21 +45,23 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/context-async-hooks": "1.11.0", + "@opentelemetry/core": "1.12.0", + "@opentelemetry/context-async-hooks": "1.12.0", "@opencensus/core": "0.1.0", - "@opentelemetry/api": ">=1.0.0 <1.5.0", + "@opentelemetry/api": "1.4.1", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "codecov": "3.8.3", "mocha": "10.0.0", "nyc": "15.1.0", + "sinon": "15.0.0", + "@types/sinon": "10.0.13", "ts-mocha": "10.0.0", "typescript": "4.4.4" }, "peerDependencies": { "@opencensus/core": "^0.1.0", - "@opentelemetry/api": ">=1.0.0 <1.5.0" + "@opentelemetry/api": "^1.0.0" }, "dependencies": { "@opentelemetry/core": "^1.0.0", @@ -68,4 +71,4 @@ }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-shim-opencensus", "sideEffects": false -} \ No newline at end of file +} diff --git a/experimental/packages/opentelemetry-shim-opencensus/src/propagation.ts b/experimental/packages/opentelemetry-shim-opencensus/src/propagation.ts new file mode 100644 index 0000000000..deb2509fa7 --- /dev/null +++ b/experimental/packages/opentelemetry-shim-opencensus/src/propagation.ts @@ -0,0 +1,78 @@ +/* + * 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, + propagation, + trace, + TextMapGetter, + TextMapSetter, +} from '@opentelemetry/api'; +import { mapSpanContext, reverseMapSpanContext } from './transform'; + +class Getter implements TextMapGetter { + constructor(private ocGetter: oc.HeaderGetter) {} + keys(): string[] { + return []; + } + get(carrier: void, key: string) { + return this.ocGetter.getHeader(key); + } +} + +class Setter implements TextMapSetter { + constructor(private ocSetter: oc.HeaderSetter) {} + set(carrier: void, key: string, value: string): void { + this.ocSetter.setHeader(key, value); + } +} + +/** + * Bridges OpenTelemetry propagation API into OpenCensus. The global OTel propagator is called + * to implement the OpenCensus propagation API. + */ +export const shimPropagation: oc.Propagation = { + extract(getter: oc.HeaderGetter): oc.SpanContext | null { + const extracted = propagation.extract( + context.active(), + null, + new Getter(getter) + ); + + const otelSc = trace.getSpanContext(extracted); + return otelSc ? reverseMapSpanContext(otelSc) : null; + }, + + inject(setter: oc.HeaderSetter, spanContext: oc.SpanContext): void { + const ctx = trace.setSpanContext( + context.active(), + mapSpanContext(spanContext) + ); + propagation.inject(ctx, null, new Setter(setter)); + }, + + generate(): oc.SpanContext { + // Reading OpenCensus code, it looks like this should generate a new random span context. + // However, it doesn't appear to be used based on my testing. Options for implementing: + // + // - Return the invalid span context + // - Use the OTel ID generator, however this package should be an API-only bridge + // - Copy implementation from OpenCensus noop-propagation.ts + throw new Error('shimPropagation.generate() is not yet implemented'); + }, +}; diff --git a/experimental/packages/opentelemetry-shim-opencensus/src/transform.ts b/experimental/packages/opentelemetry-shim-opencensus/src/transform.ts new file mode 100644 index 0000000000..e5e95f4764 --- /dev/null +++ b/experimental/packages/opentelemetry-shim-opencensus/src/transform.ts @@ -0,0 +1,105 @@ +/* + * 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 { + Attributes, + SpanContext, + SpanKind, + TimeInput, + diag, +} from '@opentelemetry/api'; +import { TraceState } from '@opentelemetry/core'; + +function exhaust(value: never) { + diag.warn('Could not handle enum value %s', value); +} + +export function mapSpanKind( + kind: oc.SpanKind | undefined +): SpanKind | undefined { + switch (kind) { + case undefined: + return undefined; + case oc.SpanKind.UNSPECIFIED: + return SpanKind.INTERNAL; + case oc.SpanKind.CLIENT: + return SpanKind.CLIENT; + case oc.SpanKind.SERVER: + return SpanKind.SERVER; + default: + exhaust(kind); + return undefined; + } +} + +export function mapSpanContext({ + spanId, + traceId, + options, + traceState, +}: oc.SpanContext): SpanContext { + return { + spanId, + traceId, + traceFlags: options ?? 0, + traceState: + traceState === undefined ? undefined : new TraceState(traceState), + }; +} + +export function reverseMapSpanContext({ + spanId, + traceId, + traceFlags, + traceState, +}: SpanContext): oc.SpanContext { + return { + spanId: spanId, + traceId: traceId, + options: traceFlags, + traceState: traceState?.serialize(), + }; +} + +// Copied from Java +// https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27 +const MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = 'message.event.type'; +const MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = + 'message.event.size.uncompressed'; +const MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = + 'message.event.size.compressed'; + +export function mapMessageEvent( + type: oc.MessageEventType, + id: number, + timestamp?: number, + uncompressedSize?: number, + compressedSize?: number +): [string, Attributes, TimeInput | undefined] { + const attributes: Attributes = { + [MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE]: oc.MessageEventType[type], + }; + if (uncompressedSize !== undefined) { + attributes[MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED] = + uncompressedSize; + } + if (compressedSize !== undefined) { + attributes[MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED] = compressedSize; + } + + return [id.toString(), attributes, timestamp]; +} diff --git a/experimental/packages/opentelemetry-shim-opencensus/test/propagation.test.ts b/experimental/packages/opentelemetry-shim-opencensus/test/propagation.test.ts new file mode 100644 index 0000000000..ce2ecb43bf --- /dev/null +++ b/experimental/packages/opentelemetry-shim-opencensus/test/propagation.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { shimPropagation } from '../src/propagation'; + +import * as oc from '@opencensus/core'; +import { propagation } from '@opentelemetry/api'; +import { W3CTraceContextPropagator } from '@opentelemetry/core'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + +const dummyGetterWithHeader: oc.HeaderGetter = { + getHeader(name) { + if (name === 'traceparent') { + return '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; + } + return undefined; + }, +}; +const dummyGetterWithoutHeader: oc.HeaderGetter = { + getHeader() { + return undefined; + }, +}; + +describe('propagation', () => { + describe('shimPropagation', () => { + beforeEach(() => { + propagation.setGlobalPropagator(new W3CTraceContextPropagator()); + }); + afterEach(() => { + propagation.disable(); + }); + + describe('extract', () => { + it('should extract when header is available', () => { + assert.deepStrictEqual(shimPropagation.extract(dummyGetterWithHeader), { + options: 1, + spanId: '00f067aa0ba902b7', + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + traceState: undefined, + }); + }); + it('should return null when header is not available', () => { + assert.deepStrictEqual( + shimPropagation.extract(dummyGetterWithoutHeader), + null + ); + }); + }); + + describe('inject', () => { + it('should inject when span context is provided', () => { + const setHeaderFake = sinon.fake<[string, string]>(); + const headerSetter: oc.HeaderSetter = { + setHeader: setHeaderFake, + }; + shimPropagation.inject(headerSetter, { + options: 1, + spanId: '00f067aa0ba902b7', + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + traceState: undefined, + }); + sinon.assert.calledWith( + setHeaderFake, + 'traceparent', + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' + ); + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-shim-opencensus/test/transform.test.ts b/experimental/packages/opentelemetry-shim-opencensus/test/transform.test.ts new file mode 100644 index 0000000000..9a7f90cda0 --- /dev/null +++ b/experimental/packages/opentelemetry-shim-opencensus/test/transform.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { + mapMessageEvent, + mapSpanContext, + mapSpanKind, + reverseMapSpanContext, +} from '../src/transform'; + +import * as oc from '@opencensus/core'; +import { SpanKind } from '@opentelemetry/api'; +import { TraceState } from '@opentelemetry/core'; +import * as assert from 'assert'; + +describe('transform', () => { + describe('mapSpanKind', () => { + it('should return undefined with undefined input', () => { + assert.strictEqual(mapSpanKind(undefined), undefined); + }); + it('should return undefined for unknown span kind', () => { + assert.strictEqual(mapSpanKind(3 as oc.SpanKind), undefined); + }); + it('should map known OC SpanKinds', () => { + assert.strictEqual( + mapSpanKind(oc.SpanKind.UNSPECIFIED), + SpanKind.INTERNAL + ); + assert.strictEqual(mapSpanKind(oc.SpanKind.CLIENT), SpanKind.CLIENT); + assert.strictEqual(mapSpanKind(oc.SpanKind.SERVER), SpanKind.SERVER); + }); + }); + + describe('mapSpanContext', () => { + it('should map everything', () => { + const sc = mapSpanContext({ + traceId: '4321', + spanId: '1234', + options: 1, + traceState: 'hello=world', + }); + assert.deepStrictEqual(sc, { + traceId: '4321', + spanId: '1234', + traceFlags: 1, + traceState: new TraceState('hello=world'), + }); + }); + it('should default trace flags to 0', () => { + const sc = mapSpanContext({ traceId: '4321', spanId: '1234' }); + assert.strictEqual(sc.traceFlags, 0); + }); + it("should not include trace state if it wasn't passed in", () => { + const sc = mapSpanContext({ + traceId: '4321', + spanId: '1234', + }); + assert.strictEqual(sc.traceState, undefined); + }); + }); + + describe('reverseMapSpanContext', () => { + it('should map everything', () => { + const sc = reverseMapSpanContext({ + traceId: '4321', + spanId: '1234', + traceFlags: 1, + traceState: new TraceState('hello=world'), + }); + assert.deepStrictEqual(sc, { + traceId: '4321', + spanId: '1234', + options: 1, + traceState: 'hello=world', + }); + }); + it("should not include trace state if it wasn't passed in", () => { + const sc = reverseMapSpanContext({ + traceId: '4321', + spanId: '1234', + traceFlags: 0, + }); + assert.strictEqual(sc.traceState, undefined); + }); + }); + + describe('mapMessageEvent', () => { + const messageEventType = oc.MessageEventType.RECEIVED; + const id = 123; + const timestamp = 321; + const uncompressedSize = 12; + const compressedSize = 15; + + it('should map message event', () => { + assert.deepStrictEqual( + mapMessageEvent( + messageEventType, + id, + timestamp, + uncompressedSize, + compressedSize + ), + [ + // event name + '123', + // attributes + { + 'message.event.size.compressed': 15, + 'message.event.size.uncompressed': 12, + 'message.event.type': 'RECEIVED', + }, + // timestamp + 321, + ] + ); + }); + it('should omit size attributes if they are not provided', () => { + assert.deepStrictEqual(mapMessageEvent(messageEventType, id, timestamp), [ + // event name + '123', + // attributes + { + 'message.event.type': 'RECEIVED', + }, + // timestamp + 321, + ]); + }); + }); +});