From 364c11a61dcfb3c3091348f880c035df0ebd987a Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 25 Mar 2019 13:34:02 -0700 Subject: [PATCH 1/2] Add HTTP text format serializer to Tag propagation component --- CHANGELOG.md | 1 + packages/opencensus-core/src/index.ts | 1 + .../src/tags/propagation/text-format.ts | 97 +++++++++++++++ .../opencensus-core/test/test-text-format.ts | 116 ++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 packages/opencensus-core/src/tags/propagation/text-format.ts create mode 100644 packages/opencensus-core/test/test-text-format.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e75ea15b..0593a8e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file. - Enforce `--strictNullChecks` and `--noUnusedLocals` Compiler Options on [opencensus-exporter-jaeger] packages. - Add support for recording Exemplars. - Add `TagMetadata` that defines the properties associated with a `Tag`. +- Add HTTP text format serializer to Tag propagation component. ## 0.0.9 - 2019-02-12 - Add Metrics API. diff --git a/packages/opencensus-core/src/index.ts b/packages/opencensus-core/src/index.ts index 190c9dd2d..fa6209ab2 100644 --- a/packages/opencensus-core/src/index.ts +++ b/packages/opencensus-core/src/index.ts @@ -72,6 +72,7 @@ export * from './stats/bucket-boundaries'; export * from './stats/metric-utils'; export * from './tags/tag-map'; export * from './tags/tagger'; +export * from './tags/propagation/text-format'; export * from './resource/resource'; // interfaces diff --git a/packages/opencensus-core/src/tags/propagation/text-format.ts b/packages/opencensus-core/src/tags/propagation/text-format.ts new file mode 100644 index 000000000..56952543b --- /dev/null +++ b/packages/opencensus-core/src/tags/propagation/text-format.ts @@ -0,0 +1,97 @@ +/** + * Copyright 2019, OpenCensus 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 + * + * http://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. + */ + +/** + * This module contains the functions for serializing and deserializing + * TagMap (TagContext) with W3C Correlation Context as the HTTP text format. + * It allows tags to propagate across requests. + * + * OpenCensus uses W3C Correlation Context as the HTTP text format. + * https://github.com/w3c/correlation-context/blob/master/correlation_context/ + * HTTP_HEADER_FORMAT.md + */ + +import {TagMap} from '../tag-map'; +import {TagKey, TagTtl, TagValue, TagValueWithMetadata} from '../types'; + +export const MAX_NUMBER_OF_TAGS = 180; +const TAG_SERIALIZED_SIZE_LIMIT = 4096; +const TAGMAP_SERIALIZED_SIZE_LIMIT = 8192; +const TAG_KEY_VALUE_DELIMITER = '='; +const TAG_DELIMITER = ','; +const UNLIMITED_PROPAGATION_MD = { + tagTtl: TagTtl.UNLIMITED_PROPAGATION +}; + +/** + * Serializes a given TagMap to the on-the-wire format based on the W3C HTTP + * text format standard. + * @param tagMap The TagMap to serialize. + */ +export function serializeTextFormat(tagMap: TagMap): string { + let ret = ''; + let totalChars = 0; + let totalTags = 0; + const tags = tagMap.tagsWithMetadata; + tags.forEach((tagsWithMetadata: TagValueWithMetadata, tagKey: TagKey) => { + if (tagsWithMetadata.tagMetadata.tagTtl !== TagTtl.NO_PROPAGATION) { + if (ret.length > 0) ret += TAG_DELIMITER; + totalChars += validateTag(tagKey, tagsWithMetadata.tagValue); + ret += tagKey.name + TAG_KEY_VALUE_DELIMITER + + tagsWithMetadata.tagValue.value; + totalTags++; + } + }); + + if (totalTags > MAX_NUMBER_OF_TAGS) { + throw new Error( + `Number of tags in the TagMap exceeds limit ${MAX_NUMBER_OF_TAGS}`); + } + + if (totalChars > TAGMAP_SERIALIZED_SIZE_LIMIT) { + throw new Error(`Size of TagMap exceeds the maximum serialized size ${ + TAGMAP_SERIALIZED_SIZE_LIMIT}`); + } + + return ret; +} + +/** + * Deserializes input to TagMap based on the W3C HTTP text format standard. + * @param str The TagMap to deserialize. + */ +export function deserializeTextFormat(str: string): TagMap { + const tags = new TagMap(); + if (!str) return tags; + const listOfTags = str.split(TAG_DELIMITER); + listOfTags.forEach((tag) => { + const keyValuePair = tag.split(TAG_KEY_VALUE_DELIMITER); + if (keyValuePair.length !== 2) throw new Error(`Malformed tag ${tag}`); + + const [name, value] = keyValuePair; + tags.set({name}, {value}, UNLIMITED_PROPAGATION_MD); + }); + return tags; +} + +function validateTag(tagKey: TagKey, tagValue: TagValue) { + const charsOfTag = tagKey.name.length + tagValue.value.length; + if (charsOfTag > TAG_SERIALIZED_SIZE_LIMIT) { + throw new Error( + `Serialized size of tag exceeds limit ${TAG_SERIALIZED_SIZE_LIMIT}`); + } + return charsOfTag; +} diff --git a/packages/opencensus-core/test/test-text-format.ts b/packages/opencensus-core/test/test-text-format.ts new file mode 100644 index 000000000..eec946558 --- /dev/null +++ b/packages/opencensus-core/test/test-text-format.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2019, OpenCensus 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 + * + * http://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 {TagMap, TagTtl} from '../src'; +import {deserializeTextFormat, MAX_NUMBER_OF_TAGS, serializeTextFormat} from '../src/tags/propagation/text-format'; + +const K1 = { + name: 'k1' +}; +const K2 = { + name: 'k2' +}; + +const V1 = { + value: 'v1' +}; +const V2 = { + value: 'v2' +}; + +describe('Text Format Serializer', () => { + const emptyTagMap = new TagMap(); + + const singleTagMap = new TagMap(); + singleTagMap.set(K1, V1); + + const multipleTagMap = new TagMap(); + multipleTagMap.set(K1, V1); + multipleTagMap.set(K2, V2); + + const NO_PROPAGATION_MD = {tagTtl: TagTtl.NO_PROPAGATION}; + const nonPropagatingTagMap = new TagMap(); + nonPropagatingTagMap.set(K1, V1, NO_PROPAGATION_MD); + + describe('serializeTextFormat', () => { + it('should serialize empty tag map', () => { + const textFormat = serializeTextFormat(emptyTagMap); + assert.equal(textFormat, ''); + }); + + it('should serialize with one tag map', () => { + const textFormat = serializeTextFormat(singleTagMap); + assert.deepEqual(textFormat, 'k1=v1'); + }); + + it('should serialize with multiple tag', () => { + const textFormat = serializeTextFormat(multipleTagMap); + assert.deepEqual(textFormat, 'k1=v1,k2=v2'); + }); + + it('should skip non propagating tag', () => { + const textFormat = serializeTextFormat(nonPropagatingTagMap); + assert.deepEqual(textFormat, ''); + }); + + it('should throw an error when exceeds the max number of tags', () => { + const tags = new TagMap(); + for (let i = 0; i < MAX_NUMBER_OF_TAGS + 1; i++) { + tags.set({name: `name-${i}`}, {value: `value-${i}`}); + } + + assert.throws(() => { + serializeTextFormat(tags); + }, /^Error: Number of tags in the TagMap exceeds limit 180/); + }); + }); + + describe('deserializeTextFormat', () => { + it('should deserialize empty string', () => { + const deserializedTagMap = deserializeTextFormat(''); + assert.deepEqual(deserializedTagMap.tags.size, 0); + }); + + it('should deserialize with one key value pair', () => { + const deserializedTagMap = deserializeTextFormat('k1=v1'); + assert.deepEqual(deserializedTagMap.tags.size, 1); + assert.deepEqual(deserializedTagMap, singleTagMap); + }); + + it('should deserialize with multiple pairs', () => { + const deserializedTagMap = deserializeTextFormat('k1=v1,k2=v2'); + assert.deepEqual(deserializedTagMap.tags.size, 2); + assert.deepEqual(deserializedTagMap, multipleTagMap); + }); + + it('should deserialize with white spaces tag', () => { + const expectedTagMap = new TagMap(); + expectedTagMap.set(K1, {value: ' v1'}); + expectedTagMap.set({name: ' k2'}, {value: 'v 2'}); + + const deserializedTagMap = deserializeTextFormat('k1= v1, k2=v 2'); + assert.deepEqual(deserializedTagMap.tags.size, 2); + assert.deepEqual(deserializedTagMap, expectedTagMap); + }); + + it('should throw an error when tags are malformed', () => { + assert.throws(() => { + deserializeTextFormat('k1,v1,k2=v2'); + }, /^Error: Malformed tag k1/); + }); + }); +}); From c05602660b2ae303668e7157fc159460e93ab579 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 25 Mar 2019 23:17:07 -0700 Subject: [PATCH 2/2] Fix link --- packages/opencensus-core/src/tags/propagation/text-format.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencensus-core/src/tags/propagation/text-format.ts b/packages/opencensus-core/src/tags/propagation/text-format.ts index 56952543b..81d2a07b8 100644 --- a/packages/opencensus-core/src/tags/propagation/text-format.ts +++ b/packages/opencensus-core/src/tags/propagation/text-format.ts @@ -20,8 +20,7 @@ * It allows tags to propagate across requests. * * OpenCensus uses W3C Correlation Context as the HTTP text format. - * https://github.com/w3c/correlation-context/blob/master/correlation_context/ - * HTTP_HEADER_FORMAT.md + * https://github.com/w3c/correlation-context/blob/master/correlation_context/HTTP_HEADER_FORMAT.md */ import {TagMap} from '../tag-map';