Skip to content

Commit

Permalink
feat: add composite propagator (#821)
Browse files Browse the repository at this point in the history
Co-authored-by: Olivier Albertini <olivier.albertini@montreal.ca>
  • Loading branch information
dyladan and OlivierAlbertini committed Mar 4, 2020
1 parent fd9c7ab commit 151106a
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 0 deletions.
78 changes: 78 additions & 0 deletions packages/opentelemetry-core/src/context/propagation/composite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*!
* 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 { Carrier, Context, HttpTextFormat, Logger } from '@opentelemetry/api';
import { NoopLogger } from '../../common/NoopLogger';
import { CompositePropagatorConfig } from './types';

/** Combines multiple propagators into a single propagator. */
export class CompositePropagator implements HttpTextFormat {
private readonly _propagators: HttpTextFormat[];
private readonly _logger: Logger;

/**
* Construct a composite propagator from a list of propagators.
*
* @param [config] Configuration object for composite propagator
*/
constructor(config: CompositePropagatorConfig = {}) {
this._propagators = config.propagators ?? [];
this._logger = config.logger ?? new NoopLogger();
}

/**
* Run each of the configured propagators with the given context and carrier.
* Propagators are run in the order they are configured, so if multiple
* propagators write the same carrier key, the propagator later in the list
* will "win".
*
* @param context Context to inject
* @param carrier Carrier into which context will be injected
*/
inject(context: Context, carrier: Carrier) {
for (const propagator of this._propagators) {
try {
propagator.inject(context, carrier);
} catch (err) {
this._logger.warn(
`Failed to inject with ${propagator.constructor.name}. Err: ${err.message}`
);
}
}
}

/**
* Run each of the configured propagators with the given context and carrier.
* Propagators are run in the order they are configured, so if multiple
* propagators write the same context key, the propagator later in the list
* will "win".
*
* @param context Context to add values to
* @param carrier Carrier from which to extract context
*/
extract(context: Context, carrier: Carrier): Context {
return this._propagators.reduce((ctx, propagator) => {
try {
return propagator.extract(ctx, carrier);
} catch (err) {
this._logger.warn(
`Failed to inject with ${propagator.constructor.name}. Err: ${err.message}`
);
}
return ctx;
}, context);
}
}
30 changes: 30 additions & 0 deletions packages/opentelemetry-core/src/context/propagation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*!
* 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 { HttpTextFormat, Logger } from '@opentelemetry/api';

/** Configuration object for composite propagator */
export interface CompositePropagatorConfig {
/**
* List of propagators to run. Propagators run in the
* list order. If a propagator later in the list writes the same context
* key as a propagator earlier in the list, the later on will "win".
*/
propagators?: HttpTextFormat[];

/** Instance of logger */
logger?: Logger;
}
2 changes: 2 additions & 0 deletions packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export * from './common/types';
export * from './version';
export * from './context/context';
export * from './context/propagation/B3Format';
export * from './context/propagation/composite';
export * from './context/propagation/HttpTraceContext';
export * from './context/propagation/types';
export * from './platform';
export * from './trace/instrumentation/BasePlugin';
export * from './trace/NoRecordingSpan';
Expand Down
160 changes: 160 additions & 0 deletions packages/opentelemetry-core/test/context/composite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*!
* Copyright 2019, 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 { Context } from '@opentelemetry/scope-base';
import * as assert from 'assert';
import {
CompositePropagator,
HttpTraceContext,
randomSpanId,
randomTraceId,
} from '../../src';
import {
setExtractedSpanContext,
getExtractedSpanContext,
} from '../../src/context/context';
import {
B3Format,
X_B3_SPAN_ID,
X_B3_TRACE_ID,
X_B3_SAMPLED,
} from '../../src/context/propagation/B3Format';
import {
TRACE_PARENT_HEADER,
TRACE_STATE_HEADER,
} from '../../src/context/propagation/HttpTraceContext';
import { TraceState } from '../../src/trace/TraceState';
import { HttpTextFormat, SpanContext } from '@opentelemetry/api';

describe('Composite Propagator', () => {
let traceId: string;
let spanId: string;

beforeEach(() => {
traceId = randomTraceId();
spanId = randomSpanId();
});

describe('inject', () => {
let carrier: { [key: string]: unknown };
let spanContext: SpanContext;
let ctxWithSpanContext: Context;

beforeEach(() => {
carrier = {};
spanContext = {
spanId,
traceId,
traceFlags: 1,
traceState: new TraceState('foo=bar'),
};
ctxWithSpanContext = setExtractedSpanContext(
Context.ROOT_CONTEXT,
spanContext
);
});

it('should inject context using all configured propagators', () => {
const composite = new CompositePropagator({
propagators: [new B3Format(), new HttpTraceContext()],
});
composite.inject(ctxWithSpanContext, carrier);

assert.strictEqual(carrier[X_B3_TRACE_ID], traceId);
assert.strictEqual(carrier[X_B3_SPAN_ID], spanId);
assert.strictEqual(carrier[X_B3_SAMPLED], 1);
assert.strictEqual(
carrier[TRACE_PARENT_HEADER],
`00-${traceId}-${spanId}-01`
);
assert.strictEqual(carrier[TRACE_STATE_HEADER], 'foo=bar');
});

it('should not throw', () => {
const composite = new CompositePropagator({
propagators: [new ThrowingPropagator(), new HttpTraceContext()],
});
composite.inject(ctxWithSpanContext, carrier);

assert.strictEqual(
carrier[TRACE_PARENT_HEADER],
`00-${traceId}-${spanId}-01`
);
});
});

describe('extract', () => {
let carrier: { [key: string]: unknown };

beforeEach(() => {
carrier = {
[X_B3_TRACE_ID]: traceId,
[X_B3_SPAN_ID]: spanId,
[X_B3_SAMPLED]: 1,
[TRACE_PARENT_HEADER]: `00-${traceId}-${spanId}-01`,
[TRACE_STATE_HEADER]: 'foo=bar',
};
});

it('should extract context using all configured propagators', () => {
const composite = new CompositePropagator({
propagators: [new B3Format(), new HttpTraceContext()],
});
const spanContext = getExtractedSpanContext(
composite.extract(Context.ROOT_CONTEXT, carrier)
);

if (!spanContext) {
throw new Error('no extracted context');
}

assert.strictEqual(spanContext.traceId, traceId);
assert.strictEqual(spanContext.spanId, spanId);
assert.strictEqual(spanContext.traceFlags, 1);
assert.strictEqual(spanContext.isRemote, true);
assert.strictEqual(spanContext.traceState!.get('foo'), 'bar');
});

it('should not throw', () => {
const composite = new CompositePropagator({
propagators: [new ThrowingPropagator(), new HttpTraceContext()],
});
const spanContext = getExtractedSpanContext(
composite.extract(Context.ROOT_CONTEXT, carrier)
);

if (!spanContext) {
throw new Error('no extracted context');
}

assert.strictEqual(spanContext.traceId, traceId);
assert.strictEqual(spanContext.spanId, spanId);
assert.strictEqual(spanContext.traceFlags, 1);
assert.strictEqual(spanContext.isRemote, true);
assert.strictEqual(spanContext.traceState!.get('foo'), 'bar');
});
});
});

class ThrowingPropagator implements HttpTextFormat {
inject(context: Context, carrier: unknown) {
throw new Error('this propagator throws');
}

extract(context: Context, carrier: unknown): Context {
throw new Error('This propagator throws');
}
}

0 comments on commit 151106a

Please sign in to comment.