From ea5615f1090b2a38251d440f0ed8f68e5d0a988f Mon Sep 17 00:00:00 2001 From: Valentin Marchaud Date: Thu, 1 Oct 2020 21:11:41 +0200 Subject: [PATCH] feat(api): propagate spanContext only using API #1456 (#1527) Co-authored-by: Bartlomiej Obecny Co-authored-by: Daniel Dyla --- api/src/context/context.ts | 123 ++++++++++++++++++ api/src/index.ts | 1 + api/src/trace/NoopTracer.ts | 31 ++++- api/test/context/context.test.ts | 80 ++++++++++++ .../noop-implementations/noop-tracer.test.ts | 40 +++++- 5 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 api/src/context/context.ts create mode 100644 api/test/context/context.test.ts diff --git a/api/src/context/context.ts b/api/src/context/context.ts new file mode 100644 index 00000000000..2b3e22a2c73 --- /dev/null +++ b/api/src/context/context.ts @@ -0,0 +1,123 @@ +/* + * 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 { Span, SpanContext } from '../'; +import { Context, createContextKey } from '@opentelemetry/context-base'; + +/** + * Active span key + */ +const ACTIVE_SPAN_KEY = createContextKey( + 'OpenTelemetry Context Key ACTIVE_SPAN' +); +const EXTRACTED_SPAN_CONTEXT_KEY = createContextKey( + 'OpenTelemetry Context Key EXTRACTED_SPAN_CONTEXT' +); +/** + * Shared key for indicating if instrumentation should be suppressed beyond + * this current scope. + */ +const SUPPRESS_INSTRUMENTATION_KEY = createContextKey( + 'OpenTelemetry Context Key SUPPRESS_INSTRUMENTATION' +); + +/** + * 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); +} + +/** + * Sets value on context to indicate that instrumentation should + * be suppressed beyond this current scope. + * + * @param context context to set the suppress instrumentation value on. + */ +export function suppressInstrumentation(context: Context): Context { + return context.setValue(SUPPRESS_INSTRUMENTATION_KEY, true); +} + +/** + * Sets value on context to indicate that instrumentation should + * no-longer be suppressed beyond this current scope. + * + * @param context context to set the suppress instrumentation value on. + */ +export function unsuppressInstrumentation(context: Context): Context { + return context.setValue(SUPPRESS_INSTRUMENTATION_KEY, false); +} + +/** + * Return current suppress instrumentation value for the given context, + * if it exists. + * + * @param context context check for the suppress instrumentation value. + */ +export function isInstrumentationSuppressed(context: Context): boolean { + return Boolean(context.getValue(SUPPRESS_INSTRUMENTATION_KEY)); +} diff --git a/api/src/index.ts b/api/src/index.ts index f68ff1c1330..e1af8b51999 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -17,6 +17,7 @@ export * from './common/Exception'; export * from './common/Logger'; export * from './common/Time'; +export * from './context/context'; export * from './context/propagation/getter'; export * from './context/propagation/TextMapPropagator'; export * from './context/propagation/NoopTextMapPropagator'; diff --git a/api/src/trace/NoopTracer.ts b/api/src/trace/NoopTracer.ts index 2fdc4dc3104..877a405cbb7 100644 --- a/api/src/trace/NoopTracer.ts +++ b/api/src/trace/NoopTracer.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { Span, SpanOptions, Tracer } from '..'; -import { NOOP_SPAN } from './NoopSpan'; +import { Span, SpanOptions, Tracer, SpanContext } from '..'; +import { Context } from '@opentelemetry/context-base'; +import { NoopSpan, NOOP_SPAN } from './NoopSpan'; +import { isSpanContextValid } from './spancontext-utils'; +import { getExtractedSpanContext } from '../context/context'; /** * No-op implementations of {@link Tracer}. @@ -26,8 +29,19 @@ export class NoopTracer implements Tracer { } // startSpan starts a noop span. - startSpan(name: string, options?: SpanOptions): Span { - return NOOP_SPAN; + startSpan(name: string, options?: SpanOptions, context?: Context): Span { + const parent = options?.parent; + const parentFromContext = context && getExtractedSpanContext(context); + if (isSpanContext(parent) && isSpanContextValid(parent)) { + return new NoopSpan(parent); + } else if ( + isSpanContext(parentFromContext) && + isSpanContextValid(parentFromContext) + ) { + return new NoopSpan(parentFromContext); + } else { + return NOOP_SPAN; + } } withSpan ReturnType>( @@ -42,4 +56,13 @@ export class NoopTracer implements Tracer { } } +function isSpanContext(spanContext: any): spanContext is SpanContext { + return ( + typeof spanContext === 'object' && + typeof spanContext['spanId'] === 'string' && + typeof spanContext['traceId'] === 'string' && + typeof spanContext['traceFlags'] === 'number' + ); +} + export const NOOP_TRACER = new NoopTracer(); diff --git a/api/test/context/context.test.ts b/api/test/context/context.test.ts new file mode 100644 index 00000000000..5c3c50929c6 --- /dev/null +++ b/api/test/context/context.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import { ROOT_CONTEXT, createContextKey } from '@opentelemetry/context-base'; +import { + suppressInstrumentation, + unsuppressInstrumentation, + isInstrumentationSuppressed, +} from '../../src/context/context'; + +const SUPPRESS_INSTRUMENTATION_KEY = createContextKey( + 'OpenTelemetry Context Key SUPPRESS_INSTRUMENTATION' +); + +describe('Context Helpers', () => { + describe('suppressInstrumentation', () => { + it('should set suppress to true', () => { + const context = suppressInstrumentation(ROOT_CONTEXT); + assert.deepStrictEqual(isInstrumentationSuppressed(context), true); + }); + }); + + describe('unsuppressInstrumentation', () => { + it('should set suppress to false', () => { + const context = unsuppressInstrumentation(ROOT_CONTEXT); + assert.deepStrictEqual(isInstrumentationSuppressed(context), false); + }); + }); + + describe('isInstrumentationSuppressed', () => { + it('should get value as bool', () => { + const expectedValue = true; + const context = ROOT_CONTEXT.setValue( + SUPPRESS_INSTRUMENTATION_KEY, + expectedValue + ); + + const value = isInstrumentationSuppressed(context); + + assert.equal(value, expectedValue); + }); + + describe('when suppress instrumentation set to null', () => { + const context = ROOT_CONTEXT.setValue(SUPPRESS_INSTRUMENTATION_KEY, null); + + it('should return false', () => { + const value = isInstrumentationSuppressed(context); + + assert.equal(value, false); + }); + }); + + describe('when suppress instrumentation set to undefined', () => { + const context = ROOT_CONTEXT.setValue( + SUPPRESS_INSTRUMENTATION_KEY, + undefined + ); + + it('should return false', () => { + const value = isInstrumentationSuppressed(context); + + assert.equal(value, false); + }); + }); + }); +}); diff --git a/api/test/noop-implementations/noop-tracer.test.ts b/api/test/noop-implementations/noop-tracer.test.ts index 62ed24bb8ee..3f68c8a14c9 100644 --- a/api/test/noop-implementations/noop-tracer.test.ts +++ b/api/test/noop-implementations/noop-tracer.test.ts @@ -15,7 +15,15 @@ */ import * as assert from 'assert'; -import { NoopTracer, NOOP_SPAN, SpanKind } from '../../src'; +import { + NoopTracer, + NOOP_SPAN, + SpanContext, + SpanKind, + TraceFlags, + context, + setExtractedSpanContext, +} from '../../src'; describe('NoopTracer', () => { it('should not crash', () => { @@ -51,4 +59,34 @@ describe('NoopTracer', () => { const patchedFn = tracer.bind(fn, NOOP_SPAN); return patchedFn(); }); + + it('should propagate valid spanContext on the span (from parent)', () => { + const tracer = new NoopTracer(); + const parent: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + const span = tracer.startSpan('test-1', { parent }); + assert(span.context().traceId === parent.traceId); + assert(span.context().spanId === parent.spanId); + assert(span.context().traceFlags === parent.traceFlags); + }); + + it('should propagate valid spanContext on the span (from context)', () => { + const tracer = new NoopTracer(); + const parent: SpanContext = { + traceId: 'd4cda95b652f4a1592b449dd92ffda3b', + spanId: '6e0c63ffe4e34c42', + traceFlags: TraceFlags.NONE, + }; + const span = tracer.startSpan( + 'test-1', + {}, + setExtractedSpanContext(context.active(), parent) + ); + assert(span.context().traceId === parent.traceId); + assert(span.context().spanId === parent.spanId); + assert(span.context().traceFlags === parent.traceFlags); + }); });