diff --git a/.circleci/config.yml b/.circleci/config.yml index 800faeed2d..f5e726e02b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,7 +40,7 @@ mysql_service: &mysql_service MYSQL_ROOT_PASSWORD: rootpw cache_1: &cache_1 - key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D paths: - ./node_modules - ./package-lock.json @@ -60,7 +60,7 @@ cache_1: &cache_1 - packages/opentelemetry-plugin-dns/node_modules cache_2: &cache_2 - key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D paths: - packages/opentelemetry-plugin-grpc/node_modules - packages/opentelemetry-plugin-http/node_modules @@ -95,10 +95,10 @@ node_unit_tests: &node_unit_tests echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - restore_cache: keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D - restore_cache: keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D - run: name: Install Root Dependencies command: npm install --ignore-scripts @@ -135,10 +135,10 @@ browsers_unit_tests: &browsers_unit_tests echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - restore_cache: keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D - restore_cache: keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-FDF2088C + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D - run: name: Install Root Dependencies command: npm install --ignore-scripts @@ -160,6 +160,8 @@ jobs: lint_&_docs: docker: - image: node:12 + environment: + NPM_CONFIG_UNSAFE_PERM: true steps: - checkout - run: @@ -171,6 +173,9 @@ jobs: - run: name: Check code style and linting command: npm run check + - run: + name: Install API dependencies + command: lerna bootstrap --scope @opentelemetry/api --include-filtered-dependencies - run: name: Docs tests command: npm run docs-test diff --git a/packages/opentelemetry-api/package.json b/packages/opentelemetry-api/package.json index d05e4f54fc..369e56079e 100644 --- a/packages/opentelemetry-api/package.json +++ b/packages/opentelemetry-api/package.json @@ -45,6 +45,9 @@ "publishConfig": { "access": "public" }, + "dependencies": { + "@opentelemetry/scope-base": "^0.4.0" + }, "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.8", diff --git a/packages/opentelemetry-api/src/context/propagation/HttpTextFormat.ts b/packages/opentelemetry-api/src/context/propagation/HttpTextFormat.ts index 273d545220..fef9293ecf 100644 --- a/packages/opentelemetry-api/src/context/propagation/HttpTextFormat.ts +++ b/packages/opentelemetry-api/src/context/propagation/HttpTextFormat.ts @@ -14,40 +14,35 @@ * limitations under the License. */ -import { SpanContext } from '../../trace/span_context'; +import { Context } from '@opentelemetry/scope-base'; import { Carrier } from './carrier'; /** - * Injects and extracts a value as text into carriers that travel in-band - * across process boundaries. Encoding is expected to conform to the HTTP + * Injects {@link Context} into and extracts it from carriers that travel + * in-band across process boundaries. Encoding is expected to conform to the HTTP * Header Field semantics. Values are often encoded as RPC/HTTP request headers. * * The carrier of propagated data on both the client (injector) and server - * (extractor) side is usually an http request. Propagation is usually - * implemented via library- specific request interceptors, where the - * client-side injects values and the server-side extracts them. + * (extractor) side is usually an object such as http headers. */ export interface HttpTextFormat { /** - * Injects the given {@link SpanContext} instance to transmit over the wire. + * Injects values from a given {@link Context} into a carrier. * * OpenTelemetry defines a common set of format values (BinaryFormat and * HTTPTextFormat), and each has an expected `carrier` type. * - * @param spanContext the SpanContext to transmit over the wire. - * @param format the format of the carrier. + * @param context the Context from which to extract values to transmit over the wire. * @param carrier the carrier of propagation fields, such as http request headers. */ - inject(spanContext: SpanContext, format: string, carrier: Carrier): void; + inject(context: Context, carrier: Carrier): void; /** - * Returns a {@link SpanContext} instance extracted from `carrier` in the - * given format from upstream. + * Given a {@link Context} and a carrier, extract context values from a carrier and + * return a new context, created from the old context, with the extracted values. * - * @param format the format of the carrier. + * @param context the Context from which to extract values to transmit over the wire. * @param carrier the carrier of propagation fields, such as http request headers. - * @returns SpanContext The extracted SpanContext, or null if no such - * SpanContext could be found in carrier. */ - extract(format: string, carrier: Carrier): SpanContext | null; + extract(context: Context, carrier: Carrier): Context; } diff --git a/packages/opentelemetry-api/src/context/propagation/NoopHttpTextFormat.ts b/packages/opentelemetry-api/src/context/propagation/NoopHttpTextFormat.ts index fb7df57604..1f38729eb5 100644 --- a/packages/opentelemetry-api/src/context/propagation/NoopHttpTextFormat.ts +++ b/packages/opentelemetry-api/src/context/propagation/NoopHttpTextFormat.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SpanContext } from '../../trace/span_context'; +import { Context } from '@opentelemetry/scope-base'; import { Carrier } from './carrier'; import { HttpTextFormat } from './HttpTextFormat'; @@ -22,11 +22,11 @@ import { HttpTextFormat } from './HttpTextFormat'; * No-op implementations of {@link HttpTextFormat}. */ export class NoopHttpTextFormat implements HttpTextFormat { - // By default does nothing - inject(spanContext: SpanContext, format: string, carrier: Carrier): void {} - // By default does nothing - extract(format: string, carrier: Carrier): SpanContext | null { - return null; + /** Noop inject function does nothing */ + inject(context: Context, carrier: Carrier): void {} + /** Noop extract function does nothing and returns the input context */ + extract(context: Context, carrier: Carrier): Context { + return context; } } diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index a453987499..4fcd63f6f7 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -46,6 +46,8 @@ export * from './trace/NoopTracerProvider'; export * from './metrics/NoopMeterProvider'; export * from './metrics/NoopMeter'; +export { Context } from '@opentelemetry/scope-base'; + import { TraceAPI } from './api/trace'; /** Entrypoint for trace API */ export const trace = TraceAPI.getInstance(); diff --git a/packages/opentelemetry-api/test/noop-implementations/noop-tracer.test.ts b/packages/opentelemetry-api/test/noop-implementations/noop-tracer.test.ts index 32dd6813ed..ed017dda99 100644 --- a/packages/opentelemetry-api/test/noop-implementations/noop-tracer.test.ts +++ b/packages/opentelemetry-api/test/noop-implementations/noop-tracer.test.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import { NoopTracer, NOOP_SPAN, SpanKind } from '../../src'; +import { Context } from '@opentelemetry/scope-base'; describe('NoopTracer', () => { it('should not crash', () => { @@ -38,8 +39,12 @@ describe('NoopTracer', () => { assert.deepStrictEqual(tracer.getCurrentSpan(), NOOP_SPAN); const httpTextFormat = tracer.getHttpTextFormat(); assert.ok(httpTextFormat); - httpTextFormat.inject(spanContext, 'HttpTextFormat', {}); - assert.deepStrictEqual(httpTextFormat.extract('HttpTextFormat', {}), null); + + httpTextFormat.inject(Context.ROOT_CONTEXT, {}); + assert.deepStrictEqual( + httpTextFormat.extract(Context.ROOT_CONTEXT, {}), + Context.ROOT_CONTEXT + ); const binaryFormat = tracer.getBinaryFormat(); assert.ok(binaryFormat); diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index 0f957c6be8..788eed895c 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -79,6 +79,7 @@ }, "dependencies": { "@opentelemetry/api": "^0.4.0", + "@opentelemetry/scope-base": "^0.4.0", "semver": "^6.3.0" } } diff --git a/packages/opentelemetry-core/src/context/context.ts b/packages/opentelemetry-core/src/context/context.ts new file mode 100644 index 0000000000..0ae080b3ed --- /dev/null +++ b/packages/opentelemetry-core/src/context/context.ts @@ -0,0 +1,83 @@ +/*! + * Copyright 2020, 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 { Span, SpanContext } from '@opentelemetry/api'; +import { Context } from '@opentelemetry/scope-base'; + +const ACTIVE_SPAN_KEY = Context.createKey( + 'OpenTelemetry Context Key ACTIVE_SPAN' +); +const EXTRACTED_SPAN_CONTEXT_KEY = Context.createKey( + 'OpenTelemetry Context Key EXTRACTED_SPAN_CONTEXT' +); + +/** + * Return the active span if one exists + * + * @param context context to get span from + */ +export function getActiveSpan(context: Context): Span | undefined { + return (context.getValue(ACTIVE_SPAN_KEY) as Span) || undefined; +} + +/** + * Set the active span on a context + * + * @param context context to use as parent + * @param span span to set active + */ +export function setActiveSpan(context: Context, span: Span): Context { + return context.setValue(ACTIVE_SPAN_KEY, span); +} + +/** + * Get the extracted span context from a context + * + * @param context context to get span context from + */ +export function getExtractedSpanContext( + context: Context +): SpanContext | undefined { + return ( + (context.getValue(EXTRACTED_SPAN_CONTEXT_KEY) as SpanContext) || undefined + ); +} + +/** + * Set the extracted span context on a context + * + * @param context context to set span context on + * @param spanContext span context to set + */ +export function setExtractedSpanContext( + context: Context, + spanContext: SpanContext +): Context { + return context.setValue(EXTRACTED_SPAN_CONTEXT_KEY, spanContext); +} + +/** + * Get the span context of the parent span if it exists, + * or the extracted span context if there is no active + * span. + * + * @param context context to get values from + */ +export function getParentSpanContext( + context: Context +): SpanContext | undefined { + return getActiveSpan(context)?.context() || getExtractedSpanContext(context); +} diff --git a/packages/opentelemetry-core/src/context/propagation/B3Format.ts b/packages/opentelemetry-core/src/context/propagation/B3Format.ts index a74e30acf4..2f2b7c9142 100644 --- a/packages/opentelemetry-core/src/context/propagation/B3Format.ts +++ b/packages/opentelemetry-core/src/context/propagation/B3Format.ts @@ -16,10 +16,11 @@ import { Carrier, + Context, HttpTextFormat, - SpanContext, TraceFlags, } from '@opentelemetry/api'; +import { getParentSpanContext, setExtractedSpanContext } from '../context'; export const X_B3_TRACE_ID = 'x-b3-traceid'; export const X_B3_SPAN_ID = 'x-b3-spanid'; @@ -41,7 +42,10 @@ function isValidSpanId(spanId: string): boolean { * Based on: https://github.com/openzipkin/b3-propagation */ export class B3Format implements HttpTextFormat { - inject(spanContext: SpanContext, format: string, carrier: Carrier): void { + inject(context: Context, carrier: Carrier) { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; + if ( isValidTraceId(spanContext.traceId) && isValidSpanId(spanContext.spanId) @@ -57,11 +61,11 @@ export class B3Format implements HttpTextFormat { } } - extract(format: string, carrier: Carrier): SpanContext | null { + extract(context: Context, carrier: Carrier): Context { const traceIdHeader = carrier[X_B3_TRACE_ID]; const spanIdHeader = carrier[X_B3_SPAN_ID]; const sampledHeader = carrier[X_B3_SAMPLED]; - if (!traceIdHeader || !spanIdHeader) return null; + if (!traceIdHeader || !spanIdHeader) return context; const traceId = Array.isArray(traceIdHeader) ? traceIdHeader[0] : traceIdHeader; @@ -71,15 +75,15 @@ export class B3Format implements HttpTextFormat { : sampledHeader; if (isValidTraceId(traceId) && isValidSpanId(spanId)) { - return { + return setExtractedSpanContext(context, { traceId, spanId, isRemote: true, traceFlags: isNaN(Number(options)) ? TraceFlags.UNSAMPLED : Number(options), - }; + }); } - return null; + return context; } } diff --git a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts index 6337035208..c1495579f1 100644 --- a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts +++ b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts @@ -16,11 +16,13 @@ import { Carrier, + Context, HttpTextFormat, SpanContext, TraceFlags, } from '@opentelemetry/api'; import { TraceState } from '../../trace/TraceState'; +import { getParentSpanContext, setExtractedSpanContext } from '../context'; export const TRACE_PARENT_HEADER = 'traceparent'; export const TRACE_STATE_HEADER = 'tracestate'; @@ -61,7 +63,10 @@ export function parseTraceParent(traceParent: string): SpanContext | null { * https://www.w3.org/TR/trace-context/ */ export class HttpTraceContext implements HttpTextFormat { - inject(spanContext: SpanContext, format: string, carrier: Carrier) { + inject(context: Context, carrier: Carrier) { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; + const traceParent = `${VERSION}-${spanContext.traceId}-${ spanContext.spanId }-0${Number(spanContext.traceFlags || TraceFlags.UNSAMPLED).toString(16)}`; @@ -72,14 +77,14 @@ export class HttpTraceContext implements HttpTextFormat { } } - extract(format: string, carrier: Carrier): SpanContext | null { + extract(context: Context, carrier: Carrier): Context { const traceParentHeader = carrier[TRACE_PARENT_HEADER]; - if (!traceParentHeader) return null; + if (!traceParentHeader) return context; const traceParent = Array.isArray(traceParentHeader) ? traceParentHeader[0] : traceParentHeader; const spanContext = parseTraceParent(traceParent); - if (!spanContext) return null; + if (!spanContext) return context; spanContext.isRemote = true; @@ -92,6 +97,6 @@ export class HttpTraceContext implements HttpTextFormat { : traceStateHeader; spanContext.traceState = new TraceState(state as string); } - return spanContext; + return setExtractedSpanContext(context, spanContext); } } diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index f1d0611e27..3038ce0b95 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -19,6 +19,7 @@ export * from './common/NoopLogger'; export * from './common/time'; export * from './common/types'; export * from './version'; +export * from './context/context'; export * from './context/propagation/B3Format'; export * from './context/propagation/BinaryTraceContext'; export * from './context/propagation/HttpTraceContext'; diff --git a/packages/opentelemetry-core/test/context/B3Format.test.ts b/packages/opentelemetry-core/test/context/B3Format.test.ts index 97087e076a..df7a1e6162 100644 --- a/packages/opentelemetry-core/test/context/B3Format.test.ts +++ b/packages/opentelemetry-core/test/context/B3Format.test.ts @@ -14,14 +14,19 @@ * limitations under the License. */ +import { SpanContext, TraceFlags } from '@opentelemetry/api'; import * as assert from 'assert'; +import { + setExtractedSpanContext, + getExtractedSpanContext, +} from '../../src/context/context'; +import { Context } from '@opentelemetry/scope-base'; import { B3Format, - X_B3_TRACE_ID, - X_B3_SPAN_ID, X_B3_SAMPLED, + X_B3_SPAN_ID, + X_B3_TRACE_ID, } from '../../src/context/propagation/B3Format'; -import { SpanContext, TraceFlags } from '@opentelemetry/api'; import { TraceState } from '../../src/trace/TraceState'; describe('B3Format', () => { @@ -40,7 +45,10 @@ describe('B3Format', () => { traceFlags: TraceFlags.SAMPLED, }; - b3Format.inject(spanContext, 'B3Format', carrier); + b3Format.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[X_B3_TRACE_ID], 'd4cda95b652f4a1592b449d5929fda1b' @@ -58,7 +66,10 @@ describe('B3Format', () => { isRemote: false, }; - b3Format.inject(spanContext, 'B3Format', carrier); + b3Format.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[X_B3_TRACE_ID], 'd4cda95b652f4a1592b449d5929fda1b' @@ -72,7 +83,10 @@ describe('B3Format', () => { traceId: '', spanId: '', }; - b3Format.inject(emptySpanContext, 'B3Format', carrier); + b3Format.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, emptySpanContext), + carrier + ); assert.deepStrictEqual(carrier[X_B3_TRACE_ID], undefined); assert.deepStrictEqual(carrier[X_B3_SPAN_ID], undefined); }); @@ -83,7 +97,10 @@ describe('B3Format', () => { spanId: '6e0c63257de34c92', }; - b3Format.inject(spanContext, 'B3Format', carrier); + b3Format.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[X_B3_TRACE_ID], 'd4cda95b652f4a1592b449d5929fda1b' @@ -97,7 +114,9 @@ describe('B3Format', () => { it('should extract context of a unsampled span from carrier', () => { carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; - const extractedSpanContext = b3Format.extract('B3Format', carrier); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -111,7 +130,9 @@ describe('B3Format', () => { carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; carrier[X_B3_SAMPLED] = '1'; - const extractedSpanContext = b3Format.extract('B3Format', carrier); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -125,7 +146,9 @@ describe('B3Format', () => { carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; carrier[X_B3_SAMPLED] = true; - const extractedSpanContext = b3Format.extract('B3Format', carrier); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -139,7 +162,9 @@ describe('B3Format', () => { carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; carrier[X_B3_SAMPLED] = false; - const extractedSpanContext = b3Format.extract('B3Format', carrier); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -149,32 +174,54 @@ describe('B3Format', () => { }); }); - it('should return null when traceId is undefined', () => { + it('should return undefined when traceId is undefined', () => { carrier[X_B3_TRACE_ID] = undefined; carrier[X_B3_SPAN_ID] = undefined; - assert.deepStrictEqual(b3Format.extract('B3Format', carrier), null); + assert.deepStrictEqual( + getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined + ); }); - it('should return null when options and spanId are undefined', () => { + it('should return undefined when options and spanId are undefined', () => { carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; carrier[X_B3_SPAN_ID] = undefined; - assert.deepStrictEqual(b3Format.extract('B3Format', carrier), null); + assert.deepStrictEqual( + getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined + ); }); - it('returns null if b3 header is missing', () => { - assert.deepStrictEqual(b3Format.extract('B3Format', carrier), null); + it('returns undefined if b3 header is missing', () => { + assert.deepStrictEqual( + getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined + ); }); - it('returns null if b3 header is invalid', () => { + it('returns undefined if b3 header is invalid', () => { carrier[X_B3_TRACE_ID] = 'invalid!'; - assert.deepStrictEqual(b3Format.extract('B3Format', carrier), null); + assert.deepStrictEqual( + getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined + ); }); it('extracts b3 from list of header', () => { carrier[X_B3_TRACE_ID] = ['0af7651916cd43dd8448eb211c80319c']; carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; carrier[X_B3_SAMPLED] = '01'; - const extractedSpanContext = b3Format.extract('B3Format', carrier); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', traceId: '0af7651916cd43dd8448eb211c80319c', @@ -185,7 +232,7 @@ describe('B3Format', () => { it('should gracefully handle an invalid b3 header', () => { // A set of test cases with different invalid combinations of a - // b3 header. These should all result in a `null` SpanContext + // b3 header. These should all result in a `undefined` SpanContext // value being extracted. const testCases: Record = { @@ -223,8 +270,10 @@ describe('B3Format', () => { Object.getOwnPropertyNames(testCases).forEach(testCase => { carrier[X_B3_TRACE_ID] = testCases[testCase]; - const extractedSpanContext = b3Format.extract('B3Format', carrier); - assert.deepStrictEqual(extractedSpanContext, null, testCase); + const extractedSpanContext = getExtractedSpanContext( + b3Format.extract(Context.ROOT_CONTEXT, carrier) + ); + assert.deepStrictEqual(extractedSpanContext, undefined, testCase); }); }); }); diff --git a/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts b/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts index 28cbf335b4..bc74b8d682 100644 --- a/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts +++ b/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts @@ -14,13 +14,18 @@ * limitations under the License. */ +import { SpanContext, TraceFlags } from '@opentelemetry/api'; +import { Context } from '@opentelemetry/scope-base'; import * as assert from 'assert'; +import { + getExtractedSpanContext, + setExtractedSpanContext, +} from '../../src/context/context'; import { HttpTraceContext, TRACE_PARENT_HEADER, TRACE_STATE_HEADER, } from '../../src/context/propagation/HttpTraceContext'; -import { SpanContext, TraceFlags } from '@opentelemetry/api'; import { TraceState } from '../../src/trace/TraceState'; describe('HttpTraceContext', () => { @@ -39,7 +44,10 @@ describe('HttpTraceContext', () => { traceFlags: TraceFlags.SAMPLED, }; - httpTraceContext.inject(spanContext, 'HttpTraceContext', carrier); + httpTraceContext.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[TRACE_PARENT_HEADER], '00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01' @@ -55,7 +63,10 @@ describe('HttpTraceContext', () => { traceState: new TraceState('foo=bar,baz=qux'), }; - httpTraceContext.inject(spanContext, '', carrier); + httpTraceContext.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[TRACE_PARENT_HEADER], '00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01' @@ -68,9 +79,8 @@ describe('HttpTraceContext', () => { it('should extract context of a sampled span from carrier', () => { carrier[TRACE_PARENT_HEADER] = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'; - const extractedSpanContext = httpTraceContext.extract( - 'HttpTraceContext', - carrier + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) ); assert.deepStrictEqual(extractedSpanContext, { @@ -83,16 +93,20 @@ describe('HttpTraceContext', () => { it('returns null if traceparent header is missing', () => { assert.deepStrictEqual( - httpTraceContext.extract('HttpTraceContext', carrier), - null + getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined ); }); it('returns null if traceparent header is invalid', () => { carrier[TRACE_PARENT_HEADER] = 'invalid!'; assert.deepStrictEqual( - httpTraceContext.extract('HttpTraceContext', carrier), - null + getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined ); }); @@ -100,9 +114,8 @@ describe('HttpTraceContext', () => { carrier[TRACE_PARENT_HEADER] = [ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', ]; - const extractedSpanContext = httpTraceContext.extract( - 'HttpTraceContext', - carrier + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -116,10 +129,10 @@ describe('HttpTraceContext', () => { carrier[TRACE_PARENT_HEADER] = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'; carrier[TRACE_STATE_HEADER] = 'foo=bar,baz=qux'; - const extractedSpanContext = httpTraceContext.extract( - 'HttpTraceContext', - carrier + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) ); + assert.deepStrictEqual( extractedSpanContext!.traceState!.get('foo'), 'bar' @@ -134,9 +147,8 @@ describe('HttpTraceContext', () => { carrier[TRACE_PARENT_HEADER] = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'; carrier[TRACE_STATE_HEADER] = ['foo=bar,baz=qux', 'quux=quuz']; - const extractedSpanContext = httpTraceContext.extract( - 'HttpTraceContext', - carrier + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) ); assert.deepStrictEqual(extractedSpanContext, { spanId: 'b7ad6b7169203331', @@ -187,11 +199,10 @@ describe('HttpTraceContext', () => { Object.getOwnPropertyNames(testCases).forEach(testCase => { carrier[TRACE_PARENT_HEADER] = testCases[testCase]; - const extractedSpanContext = httpTraceContext.extract( - 'HttpTraceContext', - carrier + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier) ); - assert.deepStrictEqual(extractedSpanContext, null, testCase); + assert.deepStrictEqual(extractedSpanContext, undefined, testCase); }); }); }); diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 35ce53636d..d11b4b0708 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -14,14 +14,20 @@ * limitations under the License. */ -import { BasePlugin, isValid } from '@opentelemetry/core'; import { CanonicalCode, + Context, Span, SpanKind, SpanOptions, Status, } from '@opentelemetry/api'; +import { + BasePlugin, + getExtractedSpanContext, + isValid, + setActiveSpan, +} from '@opentelemetry/core'; import { ClientRequest, IncomingMessage, @@ -34,7 +40,6 @@ import * as semver from 'semver'; import * as shimmer from 'shimmer'; import * as url from 'url'; import { AttributeNames } from './enums/AttributeNames'; -import { Format } from './enums/Format'; import { Err, Func, @@ -298,7 +303,11 @@ export class HttpPlugin extends BasePlugin { }), }; - const spanContext = propagation.extract(Format.HTTP, headers); + // Using context directly like this is temporary. In a future PR, context + // will be managed by the scope manager (which may be renamed to context manager?) + const spanContext = getExtractedSpanContext( + propagation.extract(Context.TODO, headers) + ); if (spanContext && isValid(spanContext)) { spanOptions.parent = spanContext; } @@ -405,9 +414,13 @@ export class HttpPlugin extends BasePlugin { }; const span = plugin._startHttpSpan(operationName, spanOptions); + + if (!options.headers) options.headers = {}; plugin._tracer .getHttpTextFormat() - .inject(span.context(), Format.HTTP, options.headers!); + // Using context directly like this is temporary. In a future PR, context + // will be managed by the scope manager (which may be renamed to context manager?) + .inject(setActiveSpan(Context.TODO, span), options.headers); const request: ClientRequest = plugin._safeExecute( span, diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index 3e4ca727fc..5c65bd16d7 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -13,23 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SpanContext, HttpTextFormat } from '@opentelemetry/api'; +import { Context, HttpTextFormat } from '@opentelemetry/api'; +import { + setExtractedSpanContext, + getParentSpanContext, +} from '@opentelemetry/core'; import * as http from 'http'; export class DummyPropagation implements HttpTextFormat { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; - extract(format: string, carrier: http.OutgoingHttpHeaders): SpanContext { - return { + extract(context: Context, carrier: http.OutgoingHttpHeaders) { + return setExtractedSpanContext(context, { traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string, spanId: DummyPropagation.SPAN_CONTEXT_KEY, - }; + }); } - inject( - spanContext: SpanContext, - format: string, - headers: { [custom: string]: string } - ): void { + inject(context: Context, headers: { [custom: string]: string }): void { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; } diff --git a/packages/opentelemetry-plugin-https/package.json b/packages/opentelemetry-plugin-https/package.json index ce24710c22..6e179098db 100644 --- a/packages/opentelemetry-plugin-https/package.json +++ b/packages/opentelemetry-plugin-https/package.json @@ -42,7 +42,6 @@ }, "devDependencies": { "@opentelemetry/node": "^0.4.0", - "@opentelemetry/scope-base": "^0.4.0", "@opentelemetry/tracing": "^0.4.0", "@types/got": "^9.6.7", "@types/mocha": "^5.2.7", diff --git a/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts index 3e4ca727fc..5c65bd16d7 100644 --- a/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts @@ -13,23 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SpanContext, HttpTextFormat } from '@opentelemetry/api'; +import { Context, HttpTextFormat } from '@opentelemetry/api'; +import { + setExtractedSpanContext, + getParentSpanContext, +} from '@opentelemetry/core'; import * as http from 'http'; export class DummyPropagation implements HttpTextFormat { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; - extract(format: string, carrier: http.OutgoingHttpHeaders): SpanContext { - return { + extract(context: Context, carrier: http.OutgoingHttpHeaders) { + return setExtractedSpanContext(context, { traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string, spanId: DummyPropagation.SPAN_CONTEXT_KEY, - }; + }); } - inject( - spanContext: SpanContext, - format: string, - headers: { [custom: string]: string } - ): void { + inject(context: Context, headers: { [custom: string]: string }): void { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; } diff --git a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts index 3796dc4fa7..cef6eff144 100644 --- a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts +++ b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts @@ -14,15 +14,16 @@ * limitations under the License. */ +import * as types from '@opentelemetry/api'; import { BasePlugin, hrTime, isUrlIgnored, isWrapped, otperformance, + setActiveSpan, urlMatches, } from '@opentelemetry/core'; -import * as types from '@opentelemetry/api'; import { addSpanNetworkEvent, getResource, @@ -32,7 +33,6 @@ import { import * as shimmer from 'shimmer'; import { AttributeNames } from './enums/AttributeNames'; import { EventNames } from './enums/EventNames'; -import { Format } from './enums/Format'; import { OpenFunction, PropagateTraceHeaderCorsUrls, @@ -90,7 +90,9 @@ export class XMLHttpRequestPlugin extends BasePlugin { const headers: { [key: string]: unknown } = {}; this._tracer .getHttpTextFormat() - .inject(span.context(), Format.HTTP, headers); + // Using context direclty like this is temporary. In a future PR, context + // will be managed by the scope manager (which may be renamed to context manager?) + .inject(setActiveSpan(types.Context.TODO, span), headers); Object.keys(headers).forEach(key => { xhr.setRequestHeader(key, String(headers[key])); diff --git a/packages/opentelemetry-propagator-jaeger/package.json b/packages/opentelemetry-propagator-jaeger/package.json index 271f18a74d..d1b56a981f 100644 --- a/packages/opentelemetry-propagator-jaeger/package.json +++ b/packages/opentelemetry-propagator-jaeger/package.json @@ -70,6 +70,7 @@ "webpack": "^4.35.2" }, "dependencies": { - "@opentelemetry/api": "^0.4.0" + "@opentelemetry/api": "^0.4.0", + "@opentelemetry/core": "^0.4.0" } } diff --git a/packages/opentelemetry-propagator-jaeger/src/JaegerHttpTraceFormat.ts b/packages/opentelemetry-propagator-jaeger/src/JaegerHttpTraceFormat.ts index 7a24288bf8..be166154c0 100644 --- a/packages/opentelemetry-propagator-jaeger/src/JaegerHttpTraceFormat.ts +++ b/packages/opentelemetry-propagator-jaeger/src/JaegerHttpTraceFormat.ts @@ -15,11 +15,16 @@ */ import { - SpanContext, + Carrier, + Context, HttpTextFormat, + SpanContext, TraceFlags, - Carrier, } from '@opentelemetry/api'; +import { + getParentSpanContext, + setExtractedSpanContext, +} from '@opentelemetry/core'; export const UBER_TRACE_ID_HEADER = 'uber-trace-id'; @@ -48,12 +53,10 @@ export class JaegerHttpTraceFormat implements HttpTextFormat { this._jaegerTraceHeader = customTraceHeader || UBER_TRACE_ID_HEADER; } - /** - * @param {SpanContext} spanContext - context from which we take information to inject in carrier. - * @param {string} format - unused. - * @param {Carrier} carrier - a carrier to which span information will be injected. - **/ - inject(spanContext: SpanContext, format: string, carrier: Carrier) { + inject(context: Context, carrier: Carrier) { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; + const traceFlags = `0${( spanContext.traceFlags || TraceFlags.UNSAMPLED ).toString(16)}`; @@ -63,19 +66,17 @@ export class JaegerHttpTraceFormat implements HttpTextFormat { ] = `${spanContext.traceId}:${spanContext.spanId}:0:${traceFlags}`; } - /** - * @param {string} format - unused. - * @param {Carrier} carrier - a carrier from which span context will be constructed. - * @return {SpanContext} - returns a span context extracted from carrier. - **/ - extract(format: string, carrier: Carrier): SpanContext | null { + extract(context: Context, carrier: Carrier): Context { const uberTraceIdHeader = carrier[this._jaegerTraceHeader]; - if (!uberTraceIdHeader) return null; + if (!uberTraceIdHeader) return context; const uberTraceId = Array.isArray(uberTraceIdHeader) ? uberTraceIdHeader[0] : uberTraceIdHeader; - return deserializeSpanContext(uberTraceId); + const spanContext = deserializeSpanContext(uberTraceId); + if (!spanContext) return context; + + return setExtractedSpanContext(context, spanContext); } } diff --git a/packages/opentelemetry-propagator-jaeger/test/JaegerHttpTraceFormat.test.ts b/packages/opentelemetry-propagator-jaeger/test/JaegerHttpTraceFormat.test.ts index 4e1ae9c9b6..c8e5e1b406 100644 --- a/packages/opentelemetry-propagator-jaeger/test/JaegerHttpTraceFormat.test.ts +++ b/packages/opentelemetry-propagator-jaeger/test/JaegerHttpTraceFormat.test.ts @@ -14,12 +14,16 @@ * limitations under the License. */ +import { Context, SpanContext, TraceFlags } from '@opentelemetry/api'; +import { + getExtractedSpanContext, + setExtractedSpanContext, +} from '@opentelemetry/core'; import * as assert from 'assert'; import { JaegerHttpTraceFormat, UBER_TRACE_ID_HEADER, } from '../src/JaegerHttpTraceFormat'; -import { SpanContext, TraceFlags } from '@opentelemetry/api'; describe('JaegerHttpTraceFormat', () => { const jaegerHttpTraceFormat = new JaegerHttpTraceFormat(); @@ -39,7 +43,10 @@ describe('JaegerHttpTraceFormat', () => { traceFlags: TraceFlags.SAMPLED, }; - jaegerHttpTraceFormat.inject(spanContext, '', carrier); + jaegerHttpTraceFormat.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[UBER_TRACE_ID_HEADER], 'd4cda95b652f4a1592b449d5929fda1b:6e0c63257de34c92:0:01' @@ -53,7 +60,10 @@ describe('JaegerHttpTraceFormat', () => { traceFlags: TraceFlags.SAMPLED, }; - customJaegerHttpTraceFormat.inject(spanContext, '', carrier); + customJaegerHttpTraceFormat.inject( + setExtractedSpanContext(Context.ROOT_CONTEXT, spanContext), + carrier + ); assert.deepStrictEqual( carrier[customHeader], 'd4cda95b652f4a1592b449d5929fda1b:6e0c63257de34c92:0:01' @@ -65,7 +75,9 @@ describe('JaegerHttpTraceFormat', () => { it('should extract context of a sampled span from carrier', () => { carrier[UBER_TRACE_ID_HEADER] = 'd4cda95b652f4a1592b449d5929fda1b:6e0c63257de34c92:0:01'; - const extractedSpanContext = jaegerHttpTraceFormat.extract('', carrier); + const extractedSpanContext = getExtractedSpanContext( + jaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: '6e0c63257de34c92', @@ -78,7 +90,9 @@ describe('JaegerHttpTraceFormat', () => { it('should extract context of a sampled span from carrier with 1 bit flag', () => { carrier[UBER_TRACE_ID_HEADER] = '9c41e35aeb6d1272:45fd2a9709dadcf1:a13699e3fb724f40:1'; - const extractedSpanContext = jaegerHttpTraceFormat.extract('', carrier); + const extractedSpanContext = getExtractedSpanContext( + jaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: '45fd2a9709dadcf1', @@ -91,7 +105,9 @@ describe('JaegerHttpTraceFormat', () => { it('should extract context of a sampled span from UTF-8 encoded carrier', () => { carrier[UBER_TRACE_ID_HEADER] = 'ac1f3dc3c2c0b06e%3A5ac292c4a11a163e%3Ac086aaa825821068%3A1'; - const extractedSpanContext = jaegerHttpTraceFormat.extract('', carrier); + const extractedSpanContext = getExtractedSpanContext( + jaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) + ); assert.deepStrictEqual(extractedSpanContext, { spanId: '5ac292c4a11a163e', @@ -104,9 +120,8 @@ describe('JaegerHttpTraceFormat', () => { it('should use custom header if provided', () => { carrier[customHeader] = 'd4cda95b652f4a1592b449d5929fda1b:6e0c63257de34c92:0:01'; - const extractedSpanContext = customJaegerHttpTraceFormat.extract( - '', - carrier + const extractedSpanContext = getExtractedSpanContext( + customJaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) ); assert.deepStrictEqual(extractedSpanContext, { @@ -117,15 +132,22 @@ describe('JaegerHttpTraceFormat', () => { }); }); - it('returns null if UBER_TRACE_ID_HEADER header is missing', () => { - assert.deepStrictEqual(jaegerHttpTraceFormat.extract('', carrier), null); + it('returns undefined if UBER_TRACE_ID_HEADER header is missing', () => { + assert.deepStrictEqual( + getExtractedSpanContext( + jaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined + ); }); - it('returns null if UBER_TRACE_ID_HEADER header is invalid', () => { + it('returns undefined if UBER_TRACE_ID_HEADER header is invalid', () => { carrier[UBER_TRACE_ID_HEADER] = 'invalid!'; assert.deepStrictEqual( - jaegerHttpTraceFormat.extract('HttpTraceContext', carrier), - null + getExtractedSpanContext( + jaegerHttpTraceFormat.extract(Context.ROOT_CONTEXT, carrier) + ), + undefined ); }); }); diff --git a/packages/opentelemetry-scope-async-hooks/src/AsyncHooksScopeManager.ts b/packages/opentelemetry-scope-async-hooks/src/AsyncHooksScopeManager.ts index f3f4ee7f78..b0d1df0c21 100644 --- a/packages/opentelemetry-scope-async-hooks/src/AsyncHooksScopeManager.ts +++ b/packages/opentelemetry-scope-async-hooks/src/AsyncHooksScopeManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ScopeManager } from '@opentelemetry/scope-base'; +import { ScopeManager, Context } from '@opentelemetry/scope-base'; import * as asyncHooks from 'async_hooks'; import { EventEmitter } from 'events'; @@ -39,7 +39,9 @@ const ADD_LISTENER_METHODS = [ export class AsyncHooksScopeManager implements ScopeManager { private _asyncHook: asyncHooks.AsyncHook; - private _scopes: { [uid: number]: unknown } = Object.create(null); + private _scopes: { + [uid: number]: Context | undefined | null; + } = Object.create(null); constructor() { this._asyncHook = asyncHooks.createHook({ @@ -49,12 +51,12 @@ export class AsyncHooksScopeManager implements ScopeManager { }); } - active(): unknown { - return this._scopes[asyncHooks.executionAsyncId()] || undefined; + active(): Context { + return this._scopes[asyncHooks.executionAsyncId()] || Context.ROOT_CONTEXT; } with ReturnType>( - scope: unknown, + scope: Context, fn: T ): ReturnType { const uid = asyncHooks.executionAsyncId(); @@ -73,7 +75,7 @@ export class AsyncHooksScopeManager implements ScopeManager { } } - bind(target: T, scope?: unknown): T { + bind(target: T, scope: Context): T { // if no specific scope to propagate is given, we use the current one if (scope === undefined) { scope = this.active(); @@ -97,7 +99,7 @@ export class AsyncHooksScopeManager implements ScopeManager { return this; } - private _bindFunction(target: T, scope?: unknown): T { + private _bindFunction(target: T, scope: Context): T { const manager = this; const contextWrapper = function(this: {}, ...args: unknown[]) { return manager.with(scope, () => target.apply(this, args)); @@ -125,7 +127,7 @@ export class AsyncHooksScopeManager implements ScopeManager { */ private _bindEventEmitter( target: T, - scope?: unknown + scope: Context ): T { const ee = (target as unknown) as PatchedEventEmitter; if (ee.__ot_listeners !== undefined) return target; @@ -205,7 +207,7 @@ export class AsyncHooksScopeManager implements ScopeManager { private _patchAddListener( ee: PatchedEventEmitter, original: Function, - scope?: unknown + scope: Context ) { const scopeManager = this; return function(this: {}, event: string, listener: Func) { diff --git a/packages/opentelemetry-scope-async-hooks/test/AsyncHooksScopeManager.test.ts b/packages/opentelemetry-scope-async-hooks/test/AsyncHooksScopeManager.test.ts index c5333786f4..ea85caf6a3 100644 --- a/packages/opentelemetry-scope-async-hooks/test/AsyncHooksScopeManager.test.ts +++ b/packages/opentelemetry-scope-async-hooks/test/AsyncHooksScopeManager.test.ts @@ -17,9 +17,11 @@ import * as assert from 'assert'; import { AsyncHooksScopeManager } from '../src'; import { EventEmitter } from 'events'; +import { Context } from '@opentelemetry/scope-base'; describe('AsyncHooksScopeManager', () => { let scopeManager: AsyncHooksScopeManager; + const key1 = Context.createKey('test key 1'); beforeEach(() => { scopeManager = new AsyncHooksScopeManager(); @@ -50,11 +52,11 @@ describe('AsyncHooksScopeManager', () => { describe('.with()', () => { it('should run the callback (null as target)', done => { - scopeManager.with(null, done); + scopeManager.with(Context.ROOT_CONTEXT, done); }); it('should run the callback (object as target)', done => { - const test = { a: 1 }; + const test = Context.ROOT_CONTEXT.setValue(key1, 1); scopeManager.with(test, () => { assert.strictEqual(scopeManager.active(), test, 'should have scope'); return done(); @@ -63,7 +65,7 @@ describe('AsyncHooksScopeManager', () => { it('should run the callback (when disabled)', done => { scopeManager.disable(); - scopeManager.with(null, () => { + scopeManager.with(Context.ROOT_CONTEXT, () => { scopeManager.enable(); return done(); }); @@ -71,7 +73,7 @@ describe('AsyncHooksScopeManager', () => { it('should rethrow errors', done => { assert.throws(() => { - scopeManager.with(null, () => { + scopeManager.with(Context.ROOT_CONTEXT, () => { throw new Error('This should be rethrown'); }); }); @@ -79,14 +81,14 @@ describe('AsyncHooksScopeManager', () => { }); it('should finally restore an old scope', done => { - const scope1 = 'scope1'; - const scope2 = 'scope2'; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 'scope1'); + const scope2 = Context.ROOT_CONTEXT.setValue(key1, 'scope2'); scopeManager.with(scope1, () => { - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); scopeManager.with(scope2, () => { - assert.strictEqual(scopeManager.active(), 'scope2'); + assert.strictEqual(scopeManager.active(), scope2); }); - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); return done(); }); }); @@ -95,18 +97,24 @@ describe('AsyncHooksScopeManager', () => { describe('.bind(function)', () => { it('should return the same target (when enabled)', () => { const test = { a: 1 }; - assert.deepStrictEqual(scopeManager.bind(test), test); + assert.deepStrictEqual( + scopeManager.bind(test, Context.ROOT_CONTEXT), + test + ); }); it('should return the same target (when disabled)', () => { scopeManager.disable(); const test = { a: 1 }; - assert.deepStrictEqual(scopeManager.bind(test), test); + assert.deepStrictEqual( + scopeManager.bind(test, Context.ROOT_CONTEXT), + test + ); scopeManager.enable(); }); it('should return current scope (when enabled)', done => { - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn = scopeManager.bind(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); return done(); @@ -120,7 +128,7 @@ describe('AsyncHooksScopeManager', () => { */ it('should return current scope (when disabled)', done => { scopeManager.disable(); - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn = scopeManager.bind(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); return done(); @@ -130,12 +138,12 @@ describe('AsyncHooksScopeManager', () => { it('should fail to return current scope (when disabled + async op)', done => { scopeManager.disable(); - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn = scopeManager.bind(() => { setTimeout(() => { assert.strictEqual( scopeManager.active(), - undefined, + Context.ROOT_CONTEXT, 'should have no scope' ); return done(); @@ -146,7 +154,7 @@ describe('AsyncHooksScopeManager', () => { it('should return current scope (when re-enabled + async op)', done => { scopeManager.enable(); - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn = scopeManager.bind(() => { setTimeout(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); @@ -160,19 +168,19 @@ describe('AsyncHooksScopeManager', () => { describe('.bind(event-emitter)', () => { it('should return the same target (when enabled)', () => { const ee = new EventEmitter(); - assert.deepStrictEqual(scopeManager.bind(ee), ee); + assert.deepStrictEqual(scopeManager.bind(ee, Context.ROOT_CONTEXT), ee); }); it('should return the same target (when disabled)', () => { const ee = new EventEmitter(); scopeManager.disable(); - assert.deepStrictEqual(scopeManager.bind(ee), ee); + assert.deepStrictEqual(scopeManager.bind(ee, Context.ROOT_CONTEXT), ee); scopeManager.enable(); }); it('should return current scope and removeListener (when enabled)', done => { const ee = new EventEmitter(); - const scope = { a: 2 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const patchedEe = scopeManager.bind(ee, scope); const handler = () => { assert.deepStrictEqual(scopeManager.active(), scope); @@ -187,7 +195,7 @@ describe('AsyncHooksScopeManager', () => { it('should return current scope and removeAllListener (when enabled)', done => { const ee = new EventEmitter(); - const scope = { a: 2 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const patchedEe = scopeManager.bind(ee, scope); const handler = () => { assert.deepStrictEqual(scopeManager.active(), scope); @@ -207,7 +215,7 @@ describe('AsyncHooksScopeManager', () => { it('should return scope (when disabled)', done => { scopeManager.disable(); const ee = new EventEmitter(); - const scope = { a: 2 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const patchedEe = scopeManager.bind(ee, scope); const handler = () => { assert.deepStrictEqual(scopeManager.active(), scope); @@ -224,11 +232,11 @@ describe('AsyncHooksScopeManager', () => { it('should not return current scope (when disabled + async op)', done => { scopeManager.disable(); const ee = new EventEmitter(); - const scope = { a: 3 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const patchedEe = scopeManager.bind(ee, scope); const handler = () => { setImmediate(() => { - assert.deepStrictEqual(scopeManager.active(), undefined); + assert.deepStrictEqual(scopeManager.active(), Context.ROOT_CONTEXT); patchedEe.removeAllListeners('test'); assert.strictEqual(patchedEe.listeners('test').length, 0); return done(); @@ -242,7 +250,7 @@ describe('AsyncHooksScopeManager', () => { it('should return current scope (when enabled + async op)', done => { scopeManager.enable(); const ee = new EventEmitter(); - const scope = { a: 3 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const patchedEe = scopeManager.bind(ee, scope); const handler = () => { setImmediate(() => { diff --git a/packages/opentelemetry-scope-base/src/NoopScopeManager.ts b/packages/opentelemetry-scope-base/src/NoopScopeManager.ts index ac32db311f..027f0a2e14 100644 --- a/packages/opentelemetry-scope-base/src/NoopScopeManager.ts +++ b/packages/opentelemetry-scope-base/src/NoopScopeManager.ts @@ -15,20 +15,21 @@ */ import * as types from './types'; +import { Context } from './context'; export class NoopScopeManager implements types.ScopeManager { - active(): unknown { - return undefined; + active(): Context { + return Context.ROOT_CONTEXT; } with ReturnType>( - scope: unknown, + scope: Context, fn: T ): ReturnType { return fn(); } - bind(target: T, scope?: unknown): T { + bind(target: T, scope?: Context): T { return target; } diff --git a/packages/opentelemetry-scope-base/src/context.ts b/packages/opentelemetry-scope-base/src/context.ts new file mode 100644 index 0000000000..8b42d7e9af --- /dev/null +++ b/packages/opentelemetry-scope-base/src/context.ts @@ -0,0 +1,83 @@ +/*! + * Copyright 2020, 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. + */ + +/** + * Class which stores and manages current context values. All methods which + * update context such as get and delete do not modify an existing context, + * but create a new one with updated values. + */ +export class Context { + private _currentContext: Map; + + /** The root context is used as the default parent context when there is no active context */ + public static readonly ROOT_CONTEXT = new Context(); + + /** + * This is another identifier to the root context which allows developers to easily search the + * codebase for direct uses of context which need to be removed in later PRs. + * + * It's existence is temporary and it should be removed when all references are fixed. + */ + public static readonly TODO = Context.ROOT_CONTEXT; + + /** Get a key to uniquely identify a context value */ + public static createKey(description: string) { + return Symbol(description); + } + + /** + * Construct a new context which inherits values from an optional parent context. + * + * @param parentContext a context from which to inherit values + */ + private constructor(parentContext?: Map) { + this._currentContext = parentContext ? new Map(parentContext) : new Map(); + } + + /** + * Get a value from the context. + * + * @param key key which identifies a context value + */ + getValue(key: symbol): unknown { + return this._currentContext.get(key); + } + + /** + * Create a new context which inherits from this context and has + * the given key set to the given value. + * + * @param key context key for which to set the value + * @param value value to set for the given key + */ + setValue(key: symbol, value: unknown): Context { + const context = new Context(this._currentContext); + context._currentContext.set(key, value); + return context; + } + + /** + * Return a new context which inherits from this context but does + * not contain a value for the given key. + * + * @param key context key for which to clear a value + */ + deleteValue(key: symbol): Context { + const context = new Context(this._currentContext); + context._currentContext.delete(key); + return context; + } +} diff --git a/packages/opentelemetry-scope-base/src/index.ts b/packages/opentelemetry-scope-base/src/index.ts index 46f028acc9..3f0c230358 100644 --- a/packages/opentelemetry-scope-base/src/index.ts +++ b/packages/opentelemetry-scope-base/src/index.ts @@ -15,4 +15,5 @@ */ export * from './types'; +export * from './context'; export * from './NoopScopeManager'; diff --git a/packages/opentelemetry-scope-base/src/types.ts b/packages/opentelemetry-scope-base/src/types.ts index 4071603c91..9bba83b504 100644 --- a/packages/opentelemetry-scope-base/src/types.ts +++ b/packages/opentelemetry-scope-base/src/types.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { Context } from './context'; + export interface ScopeManager { /** * Get the current active scope */ - active(): unknown; + active(): Context; /** * Run the fn callback with object set as the current active scope @@ -26,7 +28,7 @@ export interface ScopeManager { * @param fn A callback to be immediately run within a specific scope */ with ReturnType>( - scope: unknown, + scope: Context, fn: T ): ReturnType; @@ -35,7 +37,7 @@ export interface ScopeManager { * @param target Any object to which a scope need to be set * @param [scope] Optionally specify the scope which you want to assign */ - bind(target: T, scope?: unknown): T; + bind(target: T, scope?: Context): T; /** * Enable scope management diff --git a/packages/opentelemetry-scope-base/test/NoopScopeManager.test.ts b/packages/opentelemetry-scope-base/test/NoopScopeManager.test.ts index 4767f5793f..8daf15ded0 100644 --- a/packages/opentelemetry-scope-base/test/NoopScopeManager.test.ts +++ b/packages/opentelemetry-scope-base/test/NoopScopeManager.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { NoopScopeManager } from '../src'; +import { NoopScopeManager, Context } from '../src'; describe('NoopScopeManager', () => { let scopeManager: NoopScopeManager; @@ -39,16 +39,17 @@ describe('NoopScopeManager', () => { }); describe('.with()', () => { - it('should run the callback (null as target)', done => { - scopeManager.with(null, done); + it('should run the callback (Context.ROOT_CONTEXT as target)', done => { + scopeManager.with(Context.ROOT_CONTEXT, done); }); it('should run the callback (object as target)', done => { - const test = { a: 1 }; + const key = Context.createKey('test key 1'); + const test = Context.ROOT_CONTEXT.setValue(key, 1); scopeManager.with(test, () => { assert.strictEqual( scopeManager.active(), - undefined, + Context.ROOT_CONTEXT, 'should not have scope' ); return done(); @@ -57,7 +58,7 @@ describe('NoopScopeManager', () => { it('should run the callback (when disabled)', done => { scopeManager.disable(); - scopeManager.with(null, () => { + scopeManager.with(Context.ROOT_CONTEXT, () => { scopeManager.enable(); return done(); }); @@ -65,19 +66,19 @@ describe('NoopScopeManager', () => { }); describe('.active()', () => { - it('should always return null (when enabled)', () => { + it('should always return Context.ROOT_CONTEXT (when enabled)', () => { assert.strictEqual( scopeManager.active(), - undefined, + Context.ROOT_CONTEXT, 'should not have scope' ); }); - it('should always return null (when disabled)', () => { + it('should always return Context.ROOT_CONTEXT (when disabled)', () => { scopeManager.disable(); assert.strictEqual( scopeManager.active(), - undefined, + Context.ROOT_CONTEXT, 'should not have scope' ); scopeManager.enable(); diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts index d735e73c7d..71b3c6be45 100644 --- a/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts +++ b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ScopeManager } from '@opentelemetry/scope-base'; +import { ScopeManager, Context } from '@opentelemetry/scope-base'; import { Func, TargetWithEvents } from './types'; import { isListenerObject } from './util'; @@ -45,20 +45,20 @@ export class ZoneScopeManager implements ScopeManager { * Returns the active scope from certain zone name * @param activeZone */ - private _activeScopeFromZone( - activeZone: Zone | undefined - ): unknown | undefined { - return activeZone && activeZone.get(ZONE_SCOPE_KEY); + private _activeScopeFromZone(activeZone: Zone | undefined): Context { + return ( + (activeZone && activeZone.get(ZONE_SCOPE_KEY)) || Context.ROOT_CONTEXT + ); } /** * @param target Function to be executed within the scope * @param scope A scope (span) to be executed within target function */ - private _bindFunction(target: T, scope?: unknown): T { + private _bindFunction(target: T, scope: Context): T { const manager = this; const contextWrapper = function(this: any, ...args: unknown[]) { - return manager.with(scope, () => target.apply(this || scope, args)); + return manager.with(scope, () => target.apply(this, args)); }; Object.defineProperty(contextWrapper, 'length', { enumerable: false, @@ -73,7 +73,7 @@ export class ZoneScopeManager implements ScopeManager { * @param obj target object on which the listeners will be patched * @param scope A scope (span) to be bind to target */ - private _bindListener(obj: T, scope?: unknown): T { + private _bindListener(obj: T, scope: Context): T { const target = (obj as unknown) as TargetWithEvents; if (target.__ot_listeners !== undefined) { return obj; @@ -137,7 +137,7 @@ export class ZoneScopeManager implements ScopeManager { private _patchAddEventListener( target: TargetWithEvents, original: Function, - scope?: unknown + scope: Context ) { const scopeManager = this; @@ -183,17 +183,18 @@ export class ZoneScopeManager implements ScopeManager { /** * Returns the active scope */ - active(): unknown | undefined { + active(): Context { + if (!this._enabled) { + return Context.ROOT_CONTEXT; + } const activeZone = this._getActiveZone(); const active = this._activeScopeFromZone(activeZone); if (active) { return active; } - if (this._enabled) { - return window; - } - return undefined; + + return Context.ROOT_CONTEXT; } /** @@ -201,7 +202,7 @@ export class ZoneScopeManager implements ScopeManager { * @param target * @param scope A scope (span) to be bind to target */ - bind(target: T | TargetWithEvents, scope?: unknown): T { + bind(target: T | TargetWithEvents, scope: Context): T { // if no specific scope to propagate is given, we use the current one if (scope === undefined) { scope = this.active(); @@ -226,9 +227,6 @@ export class ZoneScopeManager implements ScopeManager { * Enables the scope manager and creates a default(root) scope */ enable(): this { - if (this._enabled) { - return this; - } this._enabled = true; return this; } @@ -241,14 +239,9 @@ export class ZoneScopeManager implements ScopeManager { * @param fn Callback function */ with ReturnType>( - scope: unknown, + scope: Context | null, fn: () => ReturnType ): ReturnType { - // if no scope use active from active zone - if (typeof scope === 'undefined' || scope === null) { - scope = this.active(); - } - const zoneName = this._createZoneName(); const newZone = this._createZone(zoneName, scope); diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts index 490d5bfe45..33e62c29e6 100644 --- a/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts +++ b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts @@ -18,11 +18,14 @@ import 'zone.js'; import * as sinon from 'sinon'; import * as assert from 'assert'; import { ZoneScopeManager } from '../src'; +import { Context } from '@opentelemetry/scope-base'; let clock: any; describe('ZoneScopeManager', () => { let scopeManager: ZoneScopeManager; + const key1 = Context.createKey('test key 1'); + const key2 = Context.createKey('test key 2'); beforeEach(() => { clock = sinon.useFakeTimers(); @@ -37,18 +40,27 @@ describe('ZoneScopeManager', () => { describe('.enable()', () => { it('should work', () => { + const ctx = Context.ROOT_CONTEXT.setValue(key1, 1); assert.doesNotThrow(() => { assert(scopeManager.enable() === scopeManager, 'should return this'); - assert(scopeManager.active() === window, 'should has root scope'); + scopeManager.with(ctx, () => { + assert(scopeManager.active() === ctx, 'should have root scope'); + }); }); }); }); describe('.disable()', () => { it('should work', () => { + const ctx = Context.ROOT_CONTEXT.setValue(key1, 1); assert.doesNotThrow(() => { assert(scopeManager.disable() === scopeManager, 'should return this'); - assert(scopeManager.active() === undefined, 'should has no scope'); + scopeManager.with(ctx, () => { + assert( + scopeManager.active() === Context.ROOT_CONTEXT, + 'should have root scope' + ); + }); }); }); }); @@ -59,7 +71,7 @@ describe('ZoneScopeManager', () => { }); it('should run the callback (object as target)', done => { - const test = { a: 1 }; + const test = Context.ROOT_CONTEXT.setValue(key1, 1); scopeManager.with(test, () => { assert.strictEqual(scopeManager.active(), test, 'should have scope'); return done(); @@ -84,22 +96,22 @@ describe('ZoneScopeManager', () => { }); it('should finally restore an old scope, including the async task', done => { - const scope1 = 'scope1'; - const scope2 = 'scope2'; - const scope3 = 'scope3'; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 'scope1'); + const scope2 = Context.ROOT_CONTEXT.setValue(key1, 'scope2'); + const scope3 = Context.ROOT_CONTEXT.setValue(key1, 'scope3'); scopeManager.with(scope1, () => { - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); scopeManager.with(scope2, () => { - assert.strictEqual(scopeManager.active(), 'scope2'); + assert.strictEqual(scopeManager.active(), scope2); scopeManager.with(scope3, () => { - assert.strictEqual(scopeManager.active(), 'scope3'); + assert.strictEqual(scopeManager.active(), scope3); }); - assert.strictEqual(scopeManager.active(), 'scope2'); + assert.strictEqual(scopeManager.active(), scope2); }); - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); setTimeout(() => { - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); done(); }, 500); clock.tick(500); @@ -108,9 +120,9 @@ describe('ZoneScopeManager', () => { }); it('should finally restore an old scope when scope is an object, including the async task', done => { - const scope1 = { a: 1 }; - const scope2 = { a: 2 }; - const scope3 = { a: 3 }; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 1); + const scope2 = Context.ROOT_CONTEXT.setValue(key1, 2); + const scope3 = Context.ROOT_CONTEXT.setValue(key1, 3); scopeManager.with(scope1, () => { assert.strictEqual(scopeManager.active(), scope1); scopeManager.with(scope2, () => { @@ -130,20 +142,29 @@ describe('ZoneScopeManager', () => { assert.strictEqual(scopeManager.active(), window); }); it('should correctly return the scopes for 3 parallel actions', () => { - const rootSpan = { name: 'rootSpan' }; + const rootSpan = Context.ROOT_CONTEXT.setValue(key1, 'root'); scopeManager.with(rootSpan, () => { assert.ok( - scopeManager.active() === rootSpan, + scopeManager.active().getValue(key1) === 'root', 'Current span is rootSpan' ); - const concurrentSpan1 = { name: 'concurrentSpan1' }; - const concurrentSpan2 = { name: 'concurrentSpan2' }; - const concurrentSpan3 = { name: 'concurrentSpan3' }; + const concurrentSpan1 = Context.ROOT_CONTEXT.setValue( + key2, + 'concurrentSpan1' + ); + const concurrentSpan2 = Context.ROOT_CONTEXT.setValue( + key2, + 'concurrentSpan2' + ); + const concurrentSpan3 = Context.ROOT_CONTEXT.setValue( + key2, + 'concurrentSpan3' + ); scopeManager.with(concurrentSpan1, () => { setTimeout(() => { assert.ok( - scopeManager.active() === concurrentSpan1, + scopeManager.active().getValue(key2) === concurrentSpan1, 'Current span is concurrentSpan1' ); }, 10); @@ -152,7 +173,7 @@ describe('ZoneScopeManager', () => { scopeManager.with(concurrentSpan2, () => { setTimeout(() => { assert.ok( - scopeManager.active() === concurrentSpan2, + scopeManager.active().getValue(key2) === concurrentSpan2, 'Current span is concurrentSpan2' ); }, 20); @@ -161,7 +182,7 @@ describe('ZoneScopeManager', () => { scopeManager.with(concurrentSpan3, () => { setTimeout(() => { assert.ok( - scopeManager.active() === concurrentSpan3, + scopeManager.active().getValue(key2) === concurrentSpan3, 'Current span is concurrentSpan3' ); }, 30); @@ -180,31 +201,38 @@ describe('ZoneScopeManager', () => { } getTitle() { - return this.title; + return (scopeManager.active().getValue(key1) as Obj).title; } } const obj1 = new Obj('a1'); + const ctx = Context.ROOT_CONTEXT.setValue(key1, obj1); obj1.title = 'a2'; const obj2 = new Obj('b1'); - const wrapper: any = scopeManager.bind(obj2.getTitle, obj1); + const wrapper: any = scopeManager.bind(obj2.getTitle, ctx); assert.ok(wrapper(), 'a2'); }); it('should return the same target (when enabled)', () => { const test = { a: 1 }; - assert.deepStrictEqual(scopeManager.bind(test), test); + assert.deepStrictEqual( + scopeManager.bind(test, Context.ROOT_CONTEXT), + test + ); }); it('should return the same target (when disabled)', () => { scopeManager.disable(); const test = { a: 1 }; - assert.deepStrictEqual(scopeManager.bind(test), test); + assert.deepStrictEqual( + scopeManager.bind(test, Context.ROOT_CONTEXT), + test + ); scopeManager.enable(); }); it('should return current scope (when enabled)', done => { - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, { a: 1 }); const fn: any = scopeManager.bind(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); return done(); @@ -212,18 +240,22 @@ describe('ZoneScopeManager', () => { fn(); }); - it('should return current scope (when disabled)', done => { + it('should return root scope (when disabled)', done => { scopeManager.disable(); - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, { a: 1 }); const fn: any = scopeManager.bind(() => { - assert.strictEqual(scopeManager.active(), scope, 'should have scope'); + assert.strictEqual( + scopeManager.active(), + Context.ROOT_CONTEXT, + 'should have scope' + ); return done(); }, scope); fn(); }); it('should bind the the certain scope to the target "addEventListener" function', done => { - const scope1 = { a: 1 }; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 1); const element = document.createElement('div'); scopeManager.bind(element, scope1); @@ -247,7 +279,7 @@ describe('ZoneScopeManager', () => { }); it('should preserve zone when creating new click event inside zone', done => { - const scope1 = { a: 1 }; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 1); const element = document.createElement('div'); scopeManager.bind(element, scope1); diff --git a/packages/opentelemetry-shim-opentracing/src/shim.ts b/packages/opentelemetry-shim-opentracing/src/shim.ts index 0bb64cbd02..07fb689ccf 100644 --- a/packages/opentelemetry-shim-opentracing/src/shim.ts +++ b/packages/opentelemetry-shim-opentracing/src/shim.ts @@ -15,7 +15,11 @@ */ import * as types from '@opentelemetry/api'; -import { NoopLogger } from '@opentelemetry/core'; +import { + getExtractedSpanContext, + NoopLogger, + setExtractedSpanContext, +} from '@opentelemetry/core'; import * as opentracing from 'opentracing'; function translateReferences( @@ -123,13 +127,20 @@ export class TracerShim extends opentracing.Tracer { carrier: types.Carrier ): void { const opentelemSpanContext: types.SpanContext = (spanContext as SpanContextShim).getSpanContext(); + if (!carrier || typeof carrier !== 'object') return; switch (format) { // tslint:disable-next-line:no-switch-case-fall-through case opentracing.FORMAT_HTTP_HEADERS: case opentracing.FORMAT_TEXT_MAP: this._tracer .getHttpTextFormat() - .inject(opentelemSpanContext, format, carrier); + .inject( + setExtractedSpanContext( + types.Context.ROOT_CONTEXT, + opentelemSpanContext + ), + carrier + ); return; case opentracing.FORMAT_BINARY: this._logger.warn( @@ -149,9 +160,11 @@ export class TracerShim extends opentracing.Tracer { // tslint:disable-next-line:no-switch-case-fall-through case opentracing.FORMAT_HTTP_HEADERS: case opentracing.FORMAT_TEXT_MAP: - const context = this._tracer - .getHttpTextFormat() - .extract(format, carrier); + const context = getExtractedSpanContext( + this._tracer + .getHttpTextFormat() + .extract(types.Context.ROOT_CONTEXT, carrier) + ); if (!context) { return null; } diff --git a/packages/opentelemetry-tracing/src/Tracer.ts b/packages/opentelemetry-tracing/src/Tracer.ts index baf4a375b3..6bff5f84d7 100644 --- a/packages/opentelemetry-tracing/src/Tracer.ts +++ b/packages/opentelemetry-tracing/src/Tracer.ts @@ -16,18 +16,20 @@ import * as types from '@opentelemetry/api'; import { - randomTraceId, + ConsoleLogger, + getActiveSpan, isValid, - randomSpanId, NoRecordingSpan, - ConsoleLogger, + randomSpanId, + randomTraceId, + setActiveSpan, } from '@opentelemetry/core'; -import { TracerConfig, TraceParams } from './types'; import { ScopeManager } from '@opentelemetry/scope-base'; +import { BasicTracerProvider } from './BasicTracerProvider'; +import { DEFAULT_CONFIG } from './config'; import { Span } from './Span'; +import { TraceParams, TracerConfig } from './types'; import { mergeConfig } from './utility'; -import { DEFAULT_CONFIG } from './config'; -import { BasicTracerProvider } from './BasicTracerProvider'; /** * This class represents a basic tracer. @@ -106,16 +108,11 @@ export class Tracer implements types.Tracer { /** * Returns the current Span from the current context. * - * If there is no Span associated with the current context, null is returned. + * If there is no Span associated with the current context, undefined is returned. */ getCurrentSpan(): types.Span | undefined { // Get the current Span from the context or null if none found. - const current = this._scopeManager.active(); - if (current === null || current === undefined) { - return; - } else { - return current as types.Span; - } + return getActiveSpan(this._scopeManager.active()); } /** @@ -126,14 +123,22 @@ export class Tracer implements types.Tracer { fn: T ): ReturnType { // Set given span to context. - return this._scopeManager.with(span, fn); + return this._scopeManager.with( + setActiveSpan(this._scopeManager.active(), span), + fn + ); } /** * Bind a span (or the current one) to the target's scope */ bind(target: T, span?: types.Span): T { - return this._scopeManager.bind(target, span); + return this._scopeManager.bind( + target, + span + ? setActiveSpan(this._scopeManager.active(), span) + : this._scopeManager.active() + ); } /** diff --git a/packages/opentelemetry-tracing/test/BasicTracerRegistry.test.ts b/packages/opentelemetry-tracing/test/BasicTracerRegistry.test.ts index ad29673636..f829de1ff7 100644 --- a/packages/opentelemetry-tracing/test/BasicTracerRegistry.test.ts +++ b/packages/opentelemetry-tracing/test/BasicTracerRegistry.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Context, TraceFlags } from '@opentelemetry/api'; import { ALWAYS_SAMPLER, BinaryTraceContext, @@ -21,10 +22,10 @@ import { NEVER_SAMPLER, NoopLogger, NoRecordingSpan, + setActiveSpan, TraceState, } from '@opentelemetry/core'; import { NoopScopeManager, ScopeManager } from '@opentelemetry/scope-base'; -import { TraceFlags } from '@opentelemetry/api'; import * as assert from 'assert'; import { BasicTracerProvider, Span } from '../src'; @@ -315,7 +316,8 @@ describe('BasicTracerProvider', () => { it('should return current span when it exists', () => { const tracer = new BasicTracerProvider({ scopeManager: { - active: () => 'foo', + active: () => + setActiveSpan(Context.ROOT_CONTEXT, ('foo' as any) as Span), } as ScopeManager, }).getTracer('default'); assert.deepStrictEqual(tracer.getCurrentSpan(), 'foo'); diff --git a/packages/opentelemetry-web/src/StackScopeManager.ts b/packages/opentelemetry-web/src/StackScopeManager.ts index c6dc866c20..72ee4712c6 100644 --- a/packages/opentelemetry-web/src/StackScopeManager.ts +++ b/packages/opentelemetry-web/src/StackScopeManager.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Context } from '@opentelemetry/api'; import { ScopeManager } from '@opentelemetry/scope-base'; /** @@ -29,14 +30,17 @@ export class StackScopeManager implements ScopeManager { /** * Keeps the reference to current scope */ - public _currentScope: unknown; + public _currentScope = Context.ROOT_CONTEXT; /** * * @param target Function to be executed within the scope * @param scope */ - private _bindFunction(target: T, scope?: unknown): T { + private _bindFunction( + target: T, + scope = Context.ROOT_CONTEXT + ): T { const manager = this; const contextWrapper = function(...args: unknown[]) { return manager.with(scope, () => target.apply(scope, args)); @@ -53,7 +57,7 @@ export class StackScopeManager implements ScopeManager { /** * Returns the active scope */ - active(): unknown { + active(): Context { return this._currentScope; } @@ -62,7 +66,7 @@ export class StackScopeManager implements ScopeManager { * @param target * @param scope */ - bind(target: T, scope?: unknown): T { + bind(target: T, scope = Context.ROOT_CONTEXT): T { // if no specific scope to propagate is given, we use the current one if (scope === undefined) { scope = this.active(); @@ -77,7 +81,7 @@ export class StackScopeManager implements ScopeManager { * Disable the scope manager (clears the current scope) */ disable(): this { - this._currentScope = undefined; + this._currentScope = Context.ROOT_CONTEXT; this._enabled = false; return this; } @@ -90,7 +94,7 @@ export class StackScopeManager implements ScopeManager { return this; } this._enabled = true; - this._currentScope = window; + this._currentScope = Context.ROOT_CONTEXT; return this; } @@ -101,15 +105,11 @@ export class StackScopeManager implements ScopeManager { * @param fn Callback function */ with ReturnType>( - scope: unknown, + scope: Context | null, fn: () => ReturnType ): ReturnType { - if (typeof scope === 'undefined' || scope === null) { - scope = window; - } - const previousScope = this._currentScope; - this._currentScope = scope; + this._currentScope = scope || Context.ROOT_CONTEXT; try { return fn.apply(scope); diff --git a/packages/opentelemetry-web/test/StackScopeManager.test.ts b/packages/opentelemetry-web/test/StackScopeManager.test.ts index 781dfa87f3..bb0e92970d 100644 --- a/packages/opentelemetry-web/test/StackScopeManager.test.ts +++ b/packages/opentelemetry-web/test/StackScopeManager.test.ts @@ -16,9 +16,11 @@ import * as assert from 'assert'; import { StackScopeManager } from '../src'; +import { Context } from '@opentelemetry/api'; describe('StackScopeManager', () => { let scopeManager: StackScopeManager; + const key1 = Context.createKey('test key 1'); beforeEach(() => { scopeManager = new StackScopeManager(); @@ -33,7 +35,10 @@ describe('StackScopeManager', () => { it('should work', () => { assert.doesNotThrow(() => { assert(scopeManager.enable() === scopeManager, 'should return this'); - assert(scopeManager.active() === window, 'should has root scope'); + assert( + scopeManager.active() === Context.ROOT_CONTEXT, + 'should have root scope' + ); }); }); }); @@ -42,7 +47,10 @@ describe('StackScopeManager', () => { it('should work', () => { assert.doesNotThrow(() => { assert(scopeManager.disable() === scopeManager, 'should return this'); - assert(scopeManager.active() === undefined, 'should has no scope'); + assert( + scopeManager.active() === Context.ROOT_CONTEXT, + 'should have no scope' + ); }); }); }); @@ -53,7 +61,7 @@ describe('StackScopeManager', () => { }); it('should run the callback (object as target)', done => { - const test = { a: 1 }; + const test = Context.ROOT_CONTEXT.setValue(key1, 1); scopeManager.with(test, () => { assert.strictEqual(scopeManager.active(), test, 'should have scope'); return done(); @@ -78,28 +86,28 @@ describe('StackScopeManager', () => { }); it('should finally restore an old scope', done => { - const scope1 = 'scope1'; - const scope2 = 'scope2'; - const scope3 = 'scope3'; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 'scope1'); + const scope2 = Context.ROOT_CONTEXT.setValue(key1, 'scope2'); + const scope3 = Context.ROOT_CONTEXT.setValue(key1, 'scope3'); scopeManager.with(scope1, () => { - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); scopeManager.with(scope2, () => { - assert.strictEqual(scopeManager.active(), 'scope2'); + assert.strictEqual(scopeManager.active(), scope2); scopeManager.with(scope3, () => { - assert.strictEqual(scopeManager.active(), 'scope3'); + assert.strictEqual(scopeManager.active(), scope3); }); - assert.strictEqual(scopeManager.active(), 'scope2'); + assert.strictEqual(scopeManager.active(), scope2); }); - assert.strictEqual(scopeManager.active(), 'scope1'); + assert.strictEqual(scopeManager.active(), scope1); return done(); }); assert.strictEqual(scopeManager.active(), window); }); it('should finally restore an old scope when scope is an object', done => { - const scope1 = { a: 1 }; - const scope2 = { a: 2 }; - const scope3 = { a: 3 }; + const scope1 = Context.ROOT_CONTEXT.setValue(key1, 1); + const scope2 = Context.ROOT_CONTEXT.setValue(key1, 2); + const scope3 = Context.ROOT_CONTEXT.setValue(key1, 3); scopeManager.with(scope1, () => { assert.strictEqual(scopeManager.active(), scope1); scopeManager.with(scope2, () => { @@ -126,31 +134,32 @@ describe('StackScopeManager', () => { } getTitle() { - return this.title; + return (scopeManager.active().getValue(key1) as Obj).title; } } const obj1 = new Obj('a1'); + const ctx = Context.ROOT_CONTEXT.setValue(key1, obj1); obj1.title = 'a2'; const obj2 = new Obj('b1'); - const wrapper: any = scopeManager.bind(obj2.getTitle, obj1); + const wrapper: any = scopeManager.bind(obj2.getTitle, ctx); assert.ok(wrapper(), 'a2'); }); it('should return the same target (when enabled)', () => { - const test = { a: 1 }; + const test = Context.ROOT_CONTEXT.setValue(key1, 1); assert.deepStrictEqual(scopeManager.bind(test), test); }); it('should return the same target (when disabled)', () => { scopeManager.disable(); - const test = { a: 1 }; + const test = Context.ROOT_CONTEXT.setValue(key1, 1); assert.deepStrictEqual(scopeManager.bind(test), test); scopeManager.enable(); }); it('should return current scope (when enabled)', done => { - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn: any = scopeManager.bind(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); return done(); @@ -160,7 +169,7 @@ describe('StackScopeManager', () => { it('should return current scope (when disabled)', done => { scopeManager.disable(); - const scope = { a: 1 }; + const scope = Context.ROOT_CONTEXT.setValue(key1, 1); const fn: any = scopeManager.bind(() => { assert.strictEqual(scopeManager.active(), scope, 'should have scope'); return done();