From 4270b45fb4562b56ab2cbb52ba811c528a84934f Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Fri, 5 Feb 2021 09:31:07 -0500 Subject: [PATCH] refactor!: specification compliant baggage (#1876) --- api/src/baggage/Baggage.ts | 50 +++++- api/src/baggage/{EntryValue.ts => Entry.ts} | 31 ++-- api/src/baggage/index.ts | 56 +++++++ api/src/baggage/internal/baggage.ts | 63 ++++++++ api/src/baggage/internal/symbol.ts | 20 +++ api/src/index.ts | 3 +- api/test/baggage/Baggage.test.ts | 161 ++++++++++++++++++++ 7 files changed, 356 insertions(+), 28 deletions(-) rename api/src/baggage/{EntryValue.ts => Entry.ts} (51%) create mode 100644 api/src/baggage/index.ts create mode 100644 api/src/baggage/internal/baggage.ts create mode 100644 api/src/baggage/internal/symbol.ts create mode 100644 api/test/baggage/Baggage.test.ts diff --git a/api/src/baggage/Baggage.ts b/api/src/baggage/Baggage.ts index f14371e7779..2876f5bd47b 100644 --- a/api/src/baggage/Baggage.ts +++ b/api/src/baggage/Baggage.ts @@ -14,16 +14,50 @@ * limitations under the License. */ -import { BaggageEntryValue } from './EntryValue'; +import { BaggageEntry } from './Entry'; /** - * Baggage represents collection of entries. Each key of - * Baggage is associated with exactly one value. Baggage - * is serializable, to facilitate propagating it not only inside the process - * but also across process boundaries. Baggage is used to annotate - * telemetry with the name:value pair Entry. Those values can be used to add - * dimension to the metric or additional contest properties to logs and traces. + * Baggage represents collection of key-value pairs with optional metadata. + * Each key of Baggage is associated with exactly one value. + * Baggage may be used to annotate and enrich telemetry data. */ export interface Baggage { - [entryKey: string]: BaggageEntryValue; + /** + * Get an entry from Baggage if it exists + * + * @param key The key which identifies the BaggageEntry + */ + getEntry(key: string): BaggageEntry | undefined; + + /** + * Get a list of all entries in the Baggage + */ + getAllEntries(): [string, BaggageEntry][]; + + /** + * Returns a new baggage with the entries from the current bag and the specified entry + * + * @param key string which identifies the baggage entry + * @param entry BaggageEntry for the given key + */ + setEntry(key: string, entry: BaggageEntry): Baggage; + + /** + * Returns a new baggage with the entries from the current bag except the removed entry + * + * @param key key identifying the entry to be removed + */ + removeEntry(key: string): Baggage; + + /** + * Returns a new baggage with the entries from the current bag except the removed entries + * + * @param key keys identifying the entries to be removed + */ + removeEntries(...key: string[]): Baggage; + + /** + * Returns a new baggage with no entries + */ + clear(): Baggage; } diff --git a/api/src/baggage/EntryValue.ts b/api/src/baggage/Entry.ts similarity index 51% rename from api/src/baggage/EntryValue.ts rename to api/src/baggage/Entry.ts index bafc7c37b0b..d249c85cecd 100644 --- a/api/src/baggage/EntryValue.ts +++ b/api/src/baggage/Entry.ts @@ -13,28 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export interface BaggageEntryValue { - /** `String` value of the `BaggageEntryValue`. */ + +import { baggageEntryMetadataSymbol } from './internal/symbol'; + +export interface BaggageEntry { + /** `String` value of the `BaggageEntry`. */ value: string; /** - * ttl is an integer that represents number of hops an entry can - * propagate. + * Metadata is an optional string property defined by the W3C baggage specification. + * It currently has no special meaning defined by the specification. */ - ttl?: BaggageEntryTtl; + metadata?: BaggageEntryMetadata; } /** - * BaggageEntryTtl is an integer that represents number of hops an entry can propagate. - * - * For now, ONLY special values (0 and -1) are supported. + * Serializable Metadata defined by the W3C baggage specification. + * It currently has no special meaning defined by the OpenTelemetry or W3C. */ -export enum BaggageEntryTtl { - /** - * NO_PROPAGATION is considered to have local context and is used within the - * process it created. - */ - NO_PROPAGATION = 0, - - /** UNLIMITED_PROPAGATION can propagate unlimited hops. */ - UNLIMITED_PROPAGATION = -1, -} +export type BaggageEntryMetadata = { toString(): string } & { + __TYPE__: typeof baggageEntryMetadataSymbol; +}; diff --git a/api/src/baggage/index.ts b/api/src/baggage/index.ts new file mode 100644 index 00000000000..ba0a297c5d4 --- /dev/null +++ b/api/src/baggage/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { Baggage } from './Baggage'; +import { BaggageEntry, BaggageEntryMetadata } from './Entry'; +import { BaggageImpl } from './internal/baggage'; +import { baggageEntryMetadataSymbol } from './internal/symbol'; + +export * from './Baggage'; +export * from './Entry'; + +/** + * Create a new Baggage with optional entries + * + * @param entries An array of baggage entries the new baggage should contain + */ +export function createBaggage( + entries: Record = {} +): Baggage { + return new BaggageImpl(new Map(Object.entries(entries))); +} + +/** + * Create a serializable BaggageEntryMetadata object from a string. + * + * @param str string metadata. Format is currently not defined by the spec and has no special meaning. + * + */ +export function baggageEntryMetadataFromString( + str: string +): BaggageEntryMetadata { + if (typeof str !== 'string') { + // @TODO log diagnostic + str = ''; + } + + return { + __TYPE__: baggageEntryMetadataSymbol, + toString() { + return str; + }, + }; +} diff --git a/api/src/baggage/internal/baggage.ts b/api/src/baggage/internal/baggage.ts new file mode 100644 index 00000000000..f9331f411ab --- /dev/null +++ b/api/src/baggage/internal/baggage.ts @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Baggage } from '../Baggage'; +import type { BaggageEntry } from '../Entry'; + +export class BaggageImpl implements Baggage { + private _entries: Map; + + constructor(entries?: Map) { + this._entries = entries ? new Map(entries) : new Map(); + } + + getEntry(key: string): BaggageEntry | undefined { + const entry = this._entries.get(key); + if (!entry) { + return undefined; + } + + return Object.assign({}, entry); + } + + getAllEntries(): [string, BaggageEntry][] { + return Array.from(this._entries.entries()).map(([k, v]) => [k, v]); + } + + setEntry(key: string, entry: BaggageEntry): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + newBaggage._entries.set(key, entry); + return newBaggage; + } + + removeEntry(key: string): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + newBaggage._entries.delete(key); + return newBaggage; + } + + removeEntries(...keys: string[]): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + for (const key of keys) { + newBaggage._entries.delete(key); + } + return newBaggage; + } + + clear(): BaggageImpl { + return new BaggageImpl(); + } +} diff --git a/api/src/baggage/internal/symbol.ts b/api/src/baggage/internal/symbol.ts new file mode 100644 index 00000000000..f4213926c98 --- /dev/null +++ b/api/src/baggage/internal/symbol.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Symbol used to make BaggageEntryMetadata an opaque type + */ +export const baggageEntryMetadataSymbol = Symbol('BaggageEntryMetadata'); diff --git a/api/src/index.ts b/api/src/index.ts index f1dcf8211a4..4c6b64ffd64 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -20,8 +20,7 @@ export * from './common/Time'; export * from './context/context'; export * from './context/propagation/TextMapPropagator'; export * from './context/propagation/NoopTextMapPropagator'; -export * from './baggage/Baggage'; -export * from './baggage/EntryValue'; +export * from './baggage'; export * from './trace/attributes'; export * from './trace/Event'; export * from './trace/link_context'; diff --git a/api/test/baggage/Baggage.test.ts b/api/test/baggage/Baggage.test.ts new file mode 100644 index 00000000000..45eb59d7b0e --- /dev/null +++ b/api/test/baggage/Baggage.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { + createBaggage, + setBaggage, + getBaggage, + ROOT_CONTEXT, + baggageEntryMetadataFromString, +} from '../../src'; + +describe('Baggage', () => { + describe('create', () => { + it('should create an empty bag', () => { + const bag = createBaggage(); + + assert.deepStrictEqual(bag.getAllEntries(), []); + }); + + it('should create a bag with entries', () => { + const meta = baggageEntryMetadataFromString('opaque string'); + const bag = createBaggage({ + key1: { value: 'value1' }, + key2: { value: 'value2', metadata: meta }, + }); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key1', { value: 'value1' }], + ['key2', { value: 'value2', metadata: meta }], + ]); + }); + }); + + describe('get', () => { + it('should not allow modification of returned entries', () => { + const bag = createBaggage().setEntry('key', { value: 'value' }); + + const entry = bag.getEntry('key'); + assert.ok(entry); + entry.value = 'mutated'; + + assert.strictEqual(bag.getEntry('key')?.value, 'value'); + }); + }); + + describe('set', () => { + it('should create a new bag when an entry is added', () => { + const bag = createBaggage(); + + const bag2 = bag.setEntry('key', { value: 'value' }); + + assert.notStrictEqual(bag, bag2); + assert.deepStrictEqual(bag.getAllEntries(), []); + assert.deepStrictEqual(bag2.getAllEntries(), [ + ['key', { value: 'value' }], + ]); + }); + }); + + describe('remove', () => { + it('should create a new bag when an entry is removed', () => { + const bag = createBaggage({ + key: { value: 'value' }, + }); + + const bag2 = bag.removeEntry('key'); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), []); + }); + + it('should create an empty bag multiple keys are removed', () => { + const bag = createBaggage({ + key: { value: 'value' }, + key1: { value: 'value1' }, + key2: { value: 'value2' }, + }); + + const bag2 = bag.removeEntries('key', 'key1'); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ['key1', { value: 'value1' }], + ['key2', { value: 'value2' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), [ + ['key2', { value: 'value2' }], + ]); + }); + + it('should create an empty bag when it cleared', () => { + const bag = createBaggage({ + key: { value: 'value' }, + key1: { value: 'value1' }, + }); + + const bag2 = bag.clear(); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ['key1', { value: 'value1' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), []); + }); + }); + + describe('context', () => { + it('should set and get a baggage from a context', () => { + const bag = createBaggage(); + + const ctx = setBaggage(ROOT_CONTEXT, bag); + + assert.strictEqual(bag, getBaggage(ctx)); + }); + }); + + describe('metadata', () => { + it('should create an opaque object which returns the string unchanged', () => { + const meta = baggageEntryMetadataFromString('this is a string'); + + assert.strictEqual(meta.toString(), 'this is a string'); + }); + + it('should return an empty string if input is invalid', () => { + //@ts-expect-error only accepts string values + const meta = baggageEntryMetadataFromString(1); + + assert.strictEqual(meta.toString(), ''); + }); + + it('should retain metadata', () => { + const bag = createBaggage({ + key: { + value: 'value', + metadata: baggageEntryMetadataFromString('meta'), + }, + }); + + assert.deepStrictEqual(bag.getEntry('key')?.metadata?.toString(), 'meta'); + }); + }); +});