From 5539c10c51d5005c81610f7c090d6a09416d2a2d Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Sat, 17 Nov 2018 15:52:43 -0800 Subject: [PATCH 1/6] Add Resource API --- .../src/internal/string-utils.ts | 41 ++++ .../opencensus-core/src/resource/resource.ts | 192 ++++++++++++++++++ .../opencensus-core/test/test-resource.ts | 93 +++++++++ .../opencensus-core/test/test-string-utils.ts | 30 +++ 4 files changed, 356 insertions(+) create mode 100644 packages/opencensus-core/src/internal/string-utils.ts create mode 100644 packages/opencensus-core/src/resource/resource.ts create mode 100644 packages/opencensus-core/test/test-resource.ts create mode 100644 packages/opencensus-core/test/test-string-utils.ts diff --git a/packages/opencensus-core/src/internal/string-utils.ts b/packages/opencensus-core/src/internal/string-utils.ts new file mode 100644 index 000000000..54d18898e --- /dev/null +++ b/packages/opencensus-core/src/internal/string-utils.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2018 Google LLC + * + * 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. + */ + +/** + * Internal utility methods for working with tag keys, tag values, and metric + * names. + */ +export class StringUtils { + /** + * Determines whether the String contains only printable characters. + * + * @param {string} str the String to be validated. + * @return whether the String contains only printable characters. + */ + static isPrintableString(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const ch: string = str.charAt(i); + if (!StringUtils.isPrintableChar(ch)) { + return false; + } + } + return true; + } + + static isPrintableChar(ch: string): boolean { + return ch >= ' ' && ch <= '~'; + } +} diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts new file mode 100644 index 000000000..c2d27489c --- /dev/null +++ b/packages/opencensus-core/src/resource/resource.ts @@ -0,0 +1,192 @@ +/** + * Copyright 2018, 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 {StringUtils} from '../internal/string-utils'; + +/** + * Resource represents a resource, which capture identifying information about + * the entities for which signals (stats or traces) are reported. It further + * provides a framework for detection of resource information from the + * environment and progressive population as signals propagate from the core + * instrumentation library to a backend's exporter. + */ +export class Resource { + /** + * The type identifier for the resource. + */ + private readonly type: string|null; + /** + * A map of labels that describe the resource. + */ + private readonly labels: StringMap; + + private static readonly MAX_LENGTH = 255; + private static readonly LABEL_LIST_SPLITTER = ','; + private static readonly LABEL_KEY_VALUE_SPLITTER = '='; + private static readonly ENV_TYPE = + Resource.parseResourceType(process.env.OC_RESOURCE_TYPE); + private static readonly ENV_LABEL_MAP = + Resource.parseResourceLabels(process.env.OC_RESOURCE_LABELS); + private static readonly ERROR_MESSAGE_INVALID_CHARS = + 'should be a ASCII string with a length greater than 0 and not exceed ' + + Resource.MAX_LENGTH + ' characters.'; + private static readonly ERROR_MESSAGE_INVALID_VALUE = + 'should be a ASCII string with a length not exceed ' + + Resource.MAX_LENGTH + ' characters.'; + + constructor(type: string, labels: StringMap) { + this.type = type; + this.labels = labels; + } + + /** + * Returns a Resource. This resource information is loaded from the + * OC_RESOURCE_TYPE and OC_RESOURCE_LABELS environment variables. + * + * @returns {Resource} + */ + static createFromEnvironmentVariables(): Resource { + return new Resource(Resource.ENV_TYPE, Resource.ENV_LABEL_MAP); + } + + /** + * Returns a Resource that runs all input resources sequentially and merges + * their results. In case a type of label key is already set, the first set + * value takes precedence. + * + * @param {Resource[]} resources the list of the resources. + * @returns {Resource} + */ + static mergeResources(resources: Resource[]): Resource { + let currentResource: Resource = null; + for (const resource of resources) { + currentResource = this.merge(currentResource, resource); + } + return currentResource; + } + + /** + * Returns the type identifier for the resource. + * + * @returns {string} + */ + getType(): string { + return this.type; + } + + /** + * Returns a map of labels that describe the resource. + * + * @returns {StringMap} + */ + getLabels(): StringMap { + return this.labels; + } + + /** + * Creates a resource type from the OC_RESOURCE_TYPE environment variable. + * + *

OC_RESOURCE_TYPE: A string that describes the type of the resource + * prefixed by a domain namespace, e.g. “kubernetes.io/container”. + */ + private static parseResourceType(rawEnvType: string): string { + if (rawEnvType && rawEnvType != null) { + if (!Resource.isValidAndNotEmpty(rawEnvType)) { + throw new Error(`Type ${Resource.ERROR_MESSAGE_INVALID_CHARS}`); + } + return rawEnvType.trim(); + } + return null; + } + + /** + * Creates a label map from the OC_RESOURCE_LABELS environment variable. + * + *

OC_RESOURCE_LABELS: A comma-separated list of labels describing the + * source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths + * are accepted as label keys. Values may be quoted or unquoted in general. If + * a value contains whitespaces, =, or " characters, it must always be quoted. + */ + private static parseResourceLabels(rawEnvLabels: string): StringMap { + const labels: StringMap = {}; + if (rawEnvLabels && rawEnvLabels != null) { + const rawLabels: string[] = + rawEnvLabels.split(this.LABEL_LIST_SPLITTER, -1); + for (const rawLabel of rawLabels) { + const keyValuePair: string[] = + rawLabel.split(this.LABEL_KEY_VALUE_SPLITTER, -1); + if (keyValuePair.length !== 2) { + continue; + } + const key: string = keyValuePair[0].trim(); + const value: string = keyValuePair[1].trim().replace('^"|"$', ''); + if (!Resource.isValidAndNotEmpty(key)) { + throw new Error(`Label key ${Resource.ERROR_MESSAGE_INVALID_CHARS}`); + } + if (!Resource.isValid(value)) { + throw new Error( + `Label value ${Resource.ERROR_MESSAGE_INVALID_VALUE}`); + } + labels[key] = value; + } + } + return labels; + } + + /** + * Returns a new, merged Resource by merging two resources. In case of + * a collision, first resource takes precedence. + */ + private static merge(resource: Resource, otherResource: Resource): Resource { + if (resource == null) { + return otherResource; + } + if (otherResource == null) { + return resource; + } + const mergedType: string = resource.getType() != null ? + resource.getType() : + otherResource.getType(); + const mergedLabelMap: StringMap = otherResource.getLabels(); + + const resourceLabels = resource.getLabels(); + for (const key of Object.keys(resourceLabels)) { + const value = resourceLabels[key]; + mergedLabelMap[key] = value; + } + return new Resource(mergedType, mergedLabelMap); + } + + + /** + * Determines whether the given String is a valid printable ASCII string with + * a length not exceed MAX_LENGTH characters. + */ + private static isValid(name: string): boolean { + return name.length <= Resource.MAX_LENGTH && + StringUtils.isPrintableString(name); + } + + /** + * Determines whether the given String is a valid printable ASCII string with + * a length greater than 0 and not exceed MAX_LENGTH characters. + */ + private static isValidAndNotEmpty(name: string): boolean { + return name && name.length > 0 && Resource.isValid(name); + } +} + +export interface StringMap { [key: string]: string; } diff --git a/packages/opencensus-core/test/test-resource.ts b/packages/opencensus-core/test/test-resource.ts new file mode 100644 index 000000000..2ec98c8c3 --- /dev/null +++ b/packages/opencensus-core/test/test-resource.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2018, 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'; + +process.env.OC_RESOURCE_TYPE = 'k8s.io/container'; +process.env.OC_RESOURCE_LABELS = + 'k8s.io/pod/name="pod-xyz-123",k8s.io/container/name="c1",k8s.io/namespace/name="default"'; + +import {Resource} from '../src/resource/resource'; + +const DEFAULT_RESOURCE = new Resource(null, {}); +const DEFAULT_RESOURCE_1 = new Resource('default', {'a': '100'}); +const RESOURCE_1 = new Resource('t1', {'a': '1', 'b': '2'}); +const RESOURCE_2 = new Resource('t2', {'a': '1', 'b': '3', 'c': '4'}); + +describe('Resource()', () => { + afterEach(() => { + delete process.env.OC_RESOURCE_TYPE; + delete process.env.OC_RESOURCE_LABELS; + }); + + it('should return resource information from environment variables', () => { + const resource = Resource.createFromEnvironmentVariables(); + const actualLabels = resource.getLabels(); + const expectedLabels: {[key: string]: string} = { + 'k8s.io/container/name': '"c1"', + 'k8s.io/namespace/name': '"default"', + 'k8s.io/pod/name': '"pod-xyz-123"' + }; + + assert.strictEqual(resource.getType(), 'k8s.io/container'); + assert.equal(Object.keys(actualLabels).length, 3); + assert.deepEqual(actualLabels, expectedLabels); + }); +}); + +describe('mergeResources()', () => { + it('merge resources with default, resource1', () => { + const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1]; + const resource = Resource.mergeResources(resources); + const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; + + assert.equal(resource.getType(), 't1'); + assert.equal(Object.keys(resource.getLabels()).length, 2); + assert.deepEqual(resource.getLabels(), expectedLabels); + }); + + it('merge resources with default, resource1, resource2 = null', () => { + const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1, null]; + const resource = Resource.mergeResources(resources); + const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; + + assert.equal(resource.getType(), 't1'); + assert.equal(Object.keys(resource.getLabels()).length, 2); + assert.deepEqual(resource.getLabels(), expectedLabels); + }); + + it('merge resources with default, resource1 = null, resource2', () => { + const resources: Resource[] = [DEFAULT_RESOURCE, null, RESOURCE_2]; + const resource = Resource.mergeResources(resources); + const expectedLabels: + {[key: string]: string} = {'a': '1', 'b': '3', 'c': '4'}; + + assert.equal(resource.getType(), 't2'); + assert.equal(Object.keys(resource.getLabels()).length, 3); + assert.deepEqual(resource.getLabels(), expectedLabels); + }); + + it('merge resources with default1, resource1, resource2', () => { + const resources: Resource[] = [DEFAULT_RESOURCE_1, RESOURCE_1, RESOURCE_2]; + const resource = Resource.mergeResources(resources); + const expectedLabels: + {[key: string]: string} = {'a': '100', 'b': '2', 'c': '4'}; + + assert.equal(resource.getType(), 'default'); + assert.equal(Object.keys(resource.getLabels()).length, 3); + assert.deepEqual(resource.getLabels(), expectedLabels); + }); +}); diff --git a/packages/opencensus-core/test/test-string-utils.ts b/packages/opencensus-core/test/test-string-utils.ts new file mode 100644 index 000000000..fed4ff924 --- /dev/null +++ b/packages/opencensus-core/test/test-string-utils.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2018, 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 {StringUtils} from '../src/internal/string-utils'; + +describe('StringUtils', () => { + it('should return true when string is printable', () => { + const isValid = StringUtils.isPrintableString('abcd'); + assert.deepStrictEqual(isValid, true); + }); + + it('should return false when string is not printable', () => { + const isValid = StringUtils.isPrintableString('\x00-\xFF'); + assert.deepStrictEqual(isValid, false); + }); +}); From 017ebbe7677d57f8ec8211db0dd5ec7f46ccac73 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 19 Nov 2018 17:17:06 -0800 Subject: [PATCH 2/6] Fix reviews --- packages/opencensus-core/src/internal/string-utils.ts | 10 ++++++++-- packages/opencensus-core/src/resource/resource.ts | 8 ++++---- packages/opencensus-core/test/test-resource.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/opencensus-core/src/internal/string-utils.ts b/packages/opencensus-core/src/internal/string-utils.ts index 54d18898e..be51d3bbf 100644 --- a/packages/opencensus-core/src/internal/string-utils.ts +++ b/packages/opencensus-core/src/internal/string-utils.ts @@ -22,8 +22,8 @@ export class StringUtils { /** * Determines whether the String contains only printable characters. * - * @param {string} str the String to be validated. - * @return whether the String contains only printable characters. + * @param {string} str The String to be validated. + * @return {boolean} Whether the String contains only printable characters. */ static isPrintableString(str: string): boolean { for (let i = 0; i < str.length; i++) { @@ -35,6 +35,12 @@ export class StringUtils { return true; } + /** + * Determines whether the Character is printable. + * + * @param {string} str The Character to be validated. + * @return {boolean} Whether the Character is printable. + */ static isPrintableChar(ch: string): boolean { return ch >= ' ' && ch <= '~'; } diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts index c2d27489c..02d48d163 100644 --- a/packages/opencensus-core/src/resource/resource.ts +++ b/packages/opencensus-core/src/resource/resource.ts @@ -103,7 +103,7 @@ export class Resource { * prefixed by a domain namespace, e.g. “kubernetes.io/container”. */ private static parseResourceType(rawEnvType: string): string { - if (rawEnvType && rawEnvType != null) { + if (rawEnvType) { if (!Resource.isValidAndNotEmpty(rawEnvType)) { throw new Error(`Type ${Resource.ERROR_MESSAGE_INVALID_CHARS}`); } @@ -122,7 +122,7 @@ export class Resource { */ private static parseResourceLabels(rawEnvLabels: string): StringMap { const labels: StringMap = {}; - if (rawEnvLabels && rawEnvLabels != null) { + if (rawEnvLabels) { const rawLabels: string[] = rawEnvLabels.split(this.LABEL_LIST_SPLITTER, -1); for (const rawLabel of rawLabels) { @@ -151,10 +151,10 @@ export class Resource { * a collision, first resource takes precedence. */ private static merge(resource: Resource, otherResource: Resource): Resource { - if (resource == null) { + if (!resource) { return otherResource; } - if (otherResource == null) { + if (!otherResource) { return resource; } const mergedType: string = resource.getType() != null ? diff --git a/packages/opencensus-core/test/test-resource.ts b/packages/opencensus-core/test/test-resource.ts index 2ec98c8c3..42f2b7901 100644 --- a/packages/opencensus-core/test/test-resource.ts +++ b/packages/opencensus-core/test/test-resource.ts @@ -90,4 +90,15 @@ describe('mergeResources()', () => { assert.equal(Object.keys(resource.getLabels()).length, 3); assert.deepEqual(resource.getLabels(), expectedLabels); }); + + it('merge resources with default, resource1 = undefined, resource2 = undefined', + () => { + const resources: Resource[] = [DEFAULT_RESOURCE_1, undefined, undefined]; + const resource = Resource.mergeResources(resources); + const expectedLabels: {[key: string]: string} = {'a': '100'}; + + assert.equal(resource.getType(), 'default'); + assert.equal(Object.keys(resource.getLabels()).length, 1); + assert.deepEqual(resource.getLabels(), expectedLabels); + }); }); From cdbc33fdbd9ffff355eef263a82250adefbe6e9e Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Sun, 25 Nov 2018 16:32:55 -0800 Subject: [PATCH 3/6] Add interface: Resource --- .../src/internal/string-utils.ts | 4 +- .../opencensus-core/src/resource/resource.ts | 122 ++++++++---------- .../opencensus-core/src/resource/types.ts | 29 +++++ .../opencensus-core/test/test-resource.ts | 71 +++++----- 4 files changed, 125 insertions(+), 101 deletions(-) create mode 100644 packages/opencensus-core/src/resource/types.ts diff --git a/packages/opencensus-core/src/internal/string-utils.ts b/packages/opencensus-core/src/internal/string-utils.ts index be51d3bbf..966a8e66a 100644 --- a/packages/opencensus-core/src/internal/string-utils.ts +++ b/packages/opencensus-core/src/internal/string-utils.ts @@ -23,7 +23,7 @@ export class StringUtils { * Determines whether the String contains only printable characters. * * @param {string} str The String to be validated. - * @return {boolean} Whether the String contains only printable characters. + * @returns {boolean} Whether the String contains only printable characters. */ static isPrintableString(str: string): boolean { for (let i = 0; i < str.length; i++) { @@ -39,7 +39,7 @@ export class StringUtils { * Determines whether the Character is printable. * * @param {string} str The Character to be validated. - * @return {boolean} Whether the Character is printable. + * @returns {boolean} Whether the Character is printable. */ static isPrintableChar(ch: string): boolean { return ch >= ' ' && ch <= '~'; diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts index 02d48d163..600e7c766 100644 --- a/packages/opencensus-core/src/resource/resource.ts +++ b/packages/opencensus-core/src/resource/resource.ts @@ -15,6 +15,7 @@ */ import {StringUtils} from '../internal/string-utils'; +import {Resource} from './types'; /** * Resource represents a resource, which capture identifying information about @@ -23,43 +24,30 @@ import {StringUtils} from '../internal/string-utils'; * environment and progressive population as signals propagate from the core * instrumentation library to a backend's exporter. */ -export class Resource { - /** - * The type identifier for the resource. - */ - private readonly type: string|null; - /** - * A map of labels that describe the resource. - */ - private readonly labels: StringMap; - +export class CoreResource { + // Type, label keys, and label values should not exceed 256 characters. private static readonly MAX_LENGTH = 255; - private static readonly LABEL_LIST_SPLITTER = ','; + private static readonly COMMA_SEPARATOR = ','; private static readonly LABEL_KEY_VALUE_SPLITTER = '='; private static readonly ENV_TYPE = - Resource.parseResourceType(process.env.OC_RESOURCE_TYPE); + CoreResource.parseResourceType(process.env.OC_RESOURCE_TYPE); private static readonly ENV_LABEL_MAP = - Resource.parseResourceLabels(process.env.OC_RESOURCE_LABELS); + CoreResource.parseResourceLabels(process.env.OC_RESOURCE_LABELS); private static readonly ERROR_MESSAGE_INVALID_CHARS = 'should be a ASCII string with a length greater than 0 and not exceed ' + - Resource.MAX_LENGTH + ' characters.'; + CoreResource.MAX_LENGTH + ' characters.'; private static readonly ERROR_MESSAGE_INVALID_VALUE = 'should be a ASCII string with a length not exceed ' + - Resource.MAX_LENGTH + ' characters.'; - - constructor(type: string, labels: StringMap) { - this.type = type; - this.labels = labels; - } + CoreResource.MAX_LENGTH + ' characters.'; /** * Returns a Resource. This resource information is loaded from the * OC_RESOURCE_TYPE and OC_RESOURCE_LABELS environment variables. * - * @returns {Resource} + * @returns {Resource} The resource. */ static createFromEnvironmentVariables(): Resource { - return new Resource(Resource.ENV_TYPE, Resource.ENV_LABEL_MAP); + return {type: CoreResource.ENV_TYPE, labels: CoreResource.ENV_LABEL_MAP}; } /** @@ -67,45 +55,30 @@ export class Resource { * their results. In case a type of label key is already set, the first set * value takes precedence. * - * @param {Resource[]} resources the list of the resources. - * @returns {Resource} + * @param {Resource[]} resources The list of the resources. + * @returns {Resource} The resource. */ static mergeResources(resources: Resource[]): Resource { - let currentResource: Resource = null; + let currentResource: Resource; for (const resource of resources) { currentResource = this.merge(currentResource, resource); } return currentResource; } - /** - * Returns the type identifier for the resource. - * - * @returns {string} - */ - getType(): string { - return this.type; - } - - /** - * Returns a map of labels that describe the resource. - * - * @returns {StringMap} - */ - getLabels(): StringMap { - return this.labels; - } - /** * Creates a resource type from the OC_RESOURCE_TYPE environment variable. * *

OC_RESOURCE_TYPE: A string that describes the type of the resource * prefixed by a domain namespace, e.g. “kubernetes.io/container”. + * + * @param {string} rawEnvType The resource type. + * @returns {string} The sanitized resource type. */ private static parseResourceType(rawEnvType: string): string { if (rawEnvType) { - if (!Resource.isValidAndNotEmpty(rawEnvType)) { - throw new Error(`Type ${Resource.ERROR_MESSAGE_INVALID_CHARS}`); + if (!CoreResource.isValidAndNotEmpty(rawEnvType)) { + throw new Error(`Type ${CoreResource.ERROR_MESSAGE_INVALID_CHARS}`); } return rawEnvType.trim(); } @@ -119,26 +92,34 @@ export class Resource { * source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths * are accepted as label keys. Values may be quoted or unquoted in general. If * a value contains whitespaces, =, or " characters, it must always be quoted. + * + * @param {string} rawEnvLabels The resource labels as a comma-seperated list + * of key/value pairs. + * @returns {[key: string]: string} The sanitized resource labels. */ - private static parseResourceLabels(rawEnvLabels: string): StringMap { - const labels: StringMap = {}; + private static parseResourceLabels(rawEnvLabels: string): + {[key: string]: string;} { + const labels: {[key: string]: string;} = {}; if (rawEnvLabels) { - const rawLabels: string[] = - rawEnvLabels.split(this.LABEL_LIST_SPLITTER, -1); + const rawLabels: string[] = rawEnvLabels.split(this.COMMA_SEPARATOR, -1); + let key, value; for (const rawLabel of rawLabels) { const keyValuePair: string[] = rawLabel.split(this.LABEL_KEY_VALUE_SPLITTER, -1); if (keyValuePair.length !== 2) { continue; } - const key: string = keyValuePair[0].trim(); - const value: string = keyValuePair[1].trim().replace('^"|"$', ''); - if (!Resource.isValidAndNotEmpty(key)) { - throw new Error(`Label key ${Resource.ERROR_MESSAGE_INVALID_CHARS}`); + [key, value] = keyValuePair; + // Leading and trailing whitespaces are trimmed. + key = key.trim(); + value = value.trim().split('^"|"$').join(''); + if (!CoreResource.isValidAndNotEmpty(key)) { + throw new Error( + `Label key ${CoreResource.ERROR_MESSAGE_INVALID_CHARS}`); } - if (!Resource.isValid(value)) { + if (!CoreResource.isValid(value)) { throw new Error( - `Label value ${Resource.ERROR_MESSAGE_INVALID_VALUE}`); + `Label value ${CoreResource.ERROR_MESSAGE_INVALID_VALUE}`); } labels[key] = value; } @@ -149,6 +130,10 @@ export class Resource { /** * Returns a new, merged Resource by merging two resources. In case of * a collision, first resource takes precedence. + * + * @param {Resource} resource The resource object. + * @param {Resource} otherResource The resource object. + * @returns {Resource} A new, merged Resource. */ private static merge(resource: Resource, otherResource: Resource): Resource { if (!resource) { @@ -157,36 +142,33 @@ export class Resource { if (!otherResource) { return resource; } - const mergedType: string = resource.getType() != null ? - resource.getType() : - otherResource.getType(); - const mergedLabelMap: StringMap = otherResource.getLabels(); - - const resourceLabels = resource.getLabels(); - for (const key of Object.keys(resourceLabels)) { - const value = resourceLabels[key]; - mergedLabelMap[key] = value; - } - return new Resource(mergedType, mergedLabelMap); + return { + type: resource.type || otherResource.type, + labels: Object.assign(otherResource.labels, resource.labels) + }; } /** * Determines whether the given String is a valid printable ASCII string with * a length not exceed MAX_LENGTH characters. + * + * @param {string} str The String to be validated. + * @returns {boolean} Whether the String is valid. */ private static isValid(name: string): boolean { - return name.length <= Resource.MAX_LENGTH && + return name.length <= CoreResource.MAX_LENGTH && StringUtils.isPrintableString(name); } /** * Determines whether the given String is a valid printable ASCII string with * a length greater than 0 and not exceed MAX_LENGTH characters. + * + * @param {string} str The String to be validated. + * @returns {boolean} Whether the String is valid and not empty. */ private static isValidAndNotEmpty(name: string): boolean { - return name && name.length > 0 && Resource.isValid(name); + return name && name.length > 0 && CoreResource.isValid(name); } } - -export interface StringMap { [key: string]: string; } diff --git a/packages/opencensus-core/src/resource/types.ts b/packages/opencensus-core/src/resource/types.ts new file mode 100644 index 000000000..8a39994e2 --- /dev/null +++ b/packages/opencensus-core/src/resource/types.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2018, 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. + */ + +/** A Resource describes the entity for which a signal was collected. */ +export interface Resource { + /** + * An optional string which describes a well-known type of resource. + */ + readonly type: string|null; + + /** + * A dictionary of labels with string keys and values that provide information + * about the entity. + */ + readonly labels: {[key: string]: string;}; +} diff --git a/packages/opencensus-core/test/test-resource.ts b/packages/opencensus-core/test/test-resource.ts index 42f2b7901..0d79bceea 100644 --- a/packages/opencensus-core/test/test-resource.ts +++ b/packages/opencensus-core/test/test-resource.ts @@ -20,12 +20,25 @@ process.env.OC_RESOURCE_TYPE = 'k8s.io/container'; process.env.OC_RESOURCE_LABELS = 'k8s.io/pod/name="pod-xyz-123",k8s.io/container/name="c1",k8s.io/namespace/name="default"'; -import {Resource} from '../src/resource/resource'; - -const DEFAULT_RESOURCE = new Resource(null, {}); -const DEFAULT_RESOURCE_1 = new Resource('default', {'a': '100'}); -const RESOURCE_1 = new Resource('t1', {'a': '1', 'b': '2'}); -const RESOURCE_2 = new Resource('t2', {'a': '1', 'b': '3', 'c': '4'}); +import {CoreResource} from '../src/resource/resource'; +import {Resource} from '../src/resource/types'; + +const DEFAULT_RESOURCE: Resource = { + type: null, + labels: {} +}; +const DEFAULT_RESOURCE_1: Resource = { + type: 'default', + labels: {'a': '100'} +}; +const RESOURCE_1: Resource = { + type: 't1', + labels: {'a': '1', 'b': '2'} +}; +const RESOURCE_2: Resource = { + type: 't2', + labels: {'a': '1', 'b': '3', 'c': '4'} +}; describe('Resource()', () => { afterEach(() => { @@ -34,15 +47,15 @@ describe('Resource()', () => { }); it('should return resource information from environment variables', () => { - const resource = Resource.createFromEnvironmentVariables(); - const actualLabels = resource.getLabels(); + const resource = CoreResource.createFromEnvironmentVariables(); + const actualLabels = resource.labels; const expectedLabels: {[key: string]: string} = { 'k8s.io/container/name': '"c1"', 'k8s.io/namespace/name': '"default"', 'k8s.io/pod/name': '"pod-xyz-123"' }; - assert.strictEqual(resource.getType(), 'k8s.io/container'); + assert.strictEqual(resource.type, 'k8s.io/container'); assert.equal(Object.keys(actualLabels).length, 3); assert.deepEqual(actualLabels, expectedLabels); }); @@ -51,54 +64,54 @@ describe('Resource()', () => { describe('mergeResources()', () => { it('merge resources with default, resource1', () => { const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1]; - const resource = Resource.mergeResources(resources); + const resource = CoreResource.mergeResources(resources); const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; - assert.equal(resource.getType(), 't1'); - assert.equal(Object.keys(resource.getLabels()).length, 2); - assert.deepEqual(resource.getLabels(), expectedLabels); + assert.equal(resource.type, 't1'); + assert.equal(Object.keys(resource.labels).length, 2); + assert.deepEqual(resource.labels, expectedLabels); }); it('merge resources with default, resource1, resource2 = null', () => { const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1, null]; - const resource = Resource.mergeResources(resources); + const resource = CoreResource.mergeResources(resources); const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; - assert.equal(resource.getType(), 't1'); - assert.equal(Object.keys(resource.getLabels()).length, 2); - assert.deepEqual(resource.getLabels(), expectedLabels); + assert.equal(resource.type, 't1'); + assert.equal(Object.keys(resource.labels).length, 2); + assert.deepEqual(resource.labels, expectedLabels); }); it('merge resources with default, resource1 = null, resource2', () => { const resources: Resource[] = [DEFAULT_RESOURCE, null, RESOURCE_2]; - const resource = Resource.mergeResources(resources); + const resource = CoreResource.mergeResources(resources); const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '3', 'c': '4'}; - assert.equal(resource.getType(), 't2'); - assert.equal(Object.keys(resource.getLabels()).length, 3); - assert.deepEqual(resource.getLabels(), expectedLabels); + assert.equal(resource.type, 't2'); + assert.equal(Object.keys(resource.labels).length, 3); + assert.deepEqual(resource.labels, expectedLabels); }); it('merge resources with default1, resource1, resource2', () => { const resources: Resource[] = [DEFAULT_RESOURCE_1, RESOURCE_1, RESOURCE_2]; - const resource = Resource.mergeResources(resources); + const resource = CoreResource.mergeResources(resources); const expectedLabels: {[key: string]: string} = {'a': '100', 'b': '2', 'c': '4'}; - assert.equal(resource.getType(), 'default'); - assert.equal(Object.keys(resource.getLabels()).length, 3); - assert.deepEqual(resource.getLabels(), expectedLabels); + assert.equal(resource.type, 'default'); + assert.equal(Object.keys(resource.labels).length, 3); + assert.deepEqual(resource.labels, expectedLabels); }); it('merge resources with default, resource1 = undefined, resource2 = undefined', () => { const resources: Resource[] = [DEFAULT_RESOURCE_1, undefined, undefined]; - const resource = Resource.mergeResources(resources); + const resource = CoreResource.mergeResources(resources); const expectedLabels: {[key: string]: string} = {'a': '100'}; - assert.equal(resource.getType(), 'default'); - assert.equal(Object.keys(resource.getLabels()).length, 1); - assert.deepEqual(resource.getLabels(), expectedLabels); + assert.equal(resource.type, 'default'); + assert.equal(Object.keys(resource.labels).length, 1); + assert.deepEqual(resource.labels, expectedLabels); }); }); From f3796a293390516c5ac540a8c90caf347b81c43b Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 26 Nov 2018 11:44:55 -0800 Subject: [PATCH 4/6] fix reviews --- .../opencensus-core/src/resource/resource.ts | 9 ++++---- .../opencensus-core/test/test-resource.ts | 23 +++++-------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts index 600e7c766..836a3d1df 100644 --- a/packages/opencensus-core/src/resource/resource.ts +++ b/packages/opencensus-core/src/resource/resource.ts @@ -69,7 +69,7 @@ export class CoreResource { /** * Creates a resource type from the OC_RESOURCE_TYPE environment variable. * - *

OC_RESOURCE_TYPE: A string that describes the type of the resource + * OC_RESOURCE_TYPE: A string that describes the type of the resource * prefixed by a domain namespace, e.g. “kubernetes.io/container”. * * @param {string} rawEnvType The resource type. @@ -88,7 +88,7 @@ export class CoreResource { /** * Creates a label map from the OC_RESOURCE_LABELS environment variable. * - *

OC_RESOURCE_LABELS: A comma-separated list of labels describing the + * OC_RESOURCE_LABELS: A comma-separated list of labels describing the * source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths * are accepted as label keys. Values may be quoted or unquoted in general. If * a value contains whitespaces, =, or " characters, it must always be quoted. @@ -102,14 +102,13 @@ export class CoreResource { const labels: {[key: string]: string;} = {}; if (rawEnvLabels) { const rawLabels: string[] = rawEnvLabels.split(this.COMMA_SEPARATOR, -1); - let key, value; for (const rawLabel of rawLabels) { const keyValuePair: string[] = rawLabel.split(this.LABEL_KEY_VALUE_SPLITTER, -1); if (keyValuePair.length !== 2) { continue; } - [key, value] = keyValuePair; + let [key, value] = keyValuePair; // Leading and trailing whitespaces are trimmed. key = key.trim(); value = value.trim().split('^"|"$').join(''); @@ -144,7 +143,7 @@ export class CoreResource { } return { type: resource.type || otherResource.type, - labels: Object.assign(otherResource.labels, resource.labels) + labels: Object.assign({}, otherResource.labels, resource.labels) }; } diff --git a/packages/opencensus-core/test/test-resource.ts b/packages/opencensus-core/test/test-resource.ts index 0d79bceea..77e69e4e9 100644 --- a/packages/opencensus-core/test/test-resource.ts +++ b/packages/opencensus-core/test/test-resource.ts @@ -23,23 +23,6 @@ process.env.OC_RESOURCE_LABELS = import {CoreResource} from '../src/resource/resource'; import {Resource} from '../src/resource/types'; -const DEFAULT_RESOURCE: Resource = { - type: null, - labels: {} -}; -const DEFAULT_RESOURCE_1: Resource = { - type: 'default', - labels: {'a': '100'} -}; -const RESOURCE_1: Resource = { - type: 't1', - labels: {'a': '1', 'b': '2'} -}; -const RESOURCE_2: Resource = { - type: 't2', - labels: {'a': '1', 'b': '3', 'c': '4'} -}; - describe('Resource()', () => { afterEach(() => { delete process.env.OC_RESOURCE_TYPE; @@ -62,6 +45,12 @@ describe('Resource()', () => { }); describe('mergeResources()', () => { + const DEFAULT_RESOURCE: Resource = {type: null, labels: {}}; + const DEFAULT_RESOURCE_1: Resource = {type: 'default', labels: {'a': '100'}}; + const RESOURCE_1: Resource = {type: 't1', labels: {'a': '1', 'b': '2'}}; + const RESOURCE_2: + Resource = {type: 't2', labels: {'a': '1', 'b': '3', 'c': '4'}}; + it('merge resources with default, resource1', () => { const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1]; const resource = CoreResource.mergeResources(resources); From bc169fe86fbff75697accdc5ddff8d2cf720ca73 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 26 Nov 2018 14:38:16 -0800 Subject: [PATCH 5/6] remove null from Resource: type --- packages/opencensus-core/src/resource/resource.ts | 7 +++++++ packages/opencensus-core/src/resource/types.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts index 836a3d1df..b3149a948 100644 --- a/packages/opencensus-core/src/resource/resource.ts +++ b/packages/opencensus-core/src/resource/resource.ts @@ -27,8 +27,15 @@ import {Resource} from './types'; export class CoreResource { // Type, label keys, and label values should not exceed 256 characters. private static readonly MAX_LENGTH = 255; + + /** + * OC_RESOURCE_LABELS is a comma-separated list of labels (key and value are + * separated by '=') describing the source in more detail, e.g. + * “key1=val1,key2=val2”. + */ private static readonly COMMA_SEPARATOR = ','; private static readonly LABEL_KEY_VALUE_SPLITTER = '='; + private static readonly ENV_TYPE = CoreResource.parseResourceType(process.env.OC_RESOURCE_TYPE); private static readonly ENV_LABEL_MAP = diff --git a/packages/opencensus-core/src/resource/types.ts b/packages/opencensus-core/src/resource/types.ts index 8a39994e2..d64742c4c 100644 --- a/packages/opencensus-core/src/resource/types.ts +++ b/packages/opencensus-core/src/resource/types.ts @@ -19,7 +19,7 @@ export interface Resource { /** * An optional string which describes a well-known type of resource. */ - readonly type: string|null; + readonly type: string; /** * A dictionary of labels with string keys and values that provide information From 46df0d49a7fd0bc0cbcf4b8caccacd818ba2b5bd Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 26 Nov 2018 16:35:14 -0800 Subject: [PATCH 6/6] Add interface for Labels --- .../opencensus-core/src/resource/resource.ts | 18 ++++++++---------- packages/opencensus-core/src/resource/types.ts | 5 ++++- packages/opencensus-core/test/test-resource.ts | 18 ++++++++---------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/opencensus-core/src/resource/resource.ts b/packages/opencensus-core/src/resource/resource.ts index b3149a948..03e4c3acf 100644 --- a/packages/opencensus-core/src/resource/resource.ts +++ b/packages/opencensus-core/src/resource/resource.ts @@ -15,7 +15,8 @@ */ import {StringUtils} from '../internal/string-utils'; -import {Resource} from './types'; + +import {Labels, Resource} from './types'; /** * Resource represents a resource, which capture identifying information about @@ -28,12 +29,10 @@ export class CoreResource { // Type, label keys, and label values should not exceed 256 characters. private static readonly MAX_LENGTH = 255; - /** - * OC_RESOURCE_LABELS is a comma-separated list of labels (key and value are - * separated by '=') describing the source in more detail, e.g. - * “key1=val1,key2=val2”. - */ + // OC_RESOURCE_LABELS is a comma-separated list of labels. private static readonly COMMA_SEPARATOR = ','; + + // OC_RESOURCE_LABELS contains key value pair separated by '='. private static readonly LABEL_KEY_VALUE_SPLITTER = '='; private static readonly ENV_TYPE = @@ -102,11 +101,10 @@ export class CoreResource { * * @param {string} rawEnvLabels The resource labels as a comma-seperated list * of key/value pairs. - * @returns {[key: string]: string} The sanitized resource labels. + * @returns {Labels} The sanitized resource labels. */ - private static parseResourceLabels(rawEnvLabels: string): - {[key: string]: string;} { - const labels: {[key: string]: string;} = {}; + private static parseResourceLabels(rawEnvLabels: string): Labels { + const labels: Labels = {}; if (rawEnvLabels) { const rawLabels: string[] = rawEnvLabels.split(this.COMMA_SEPARATOR, -1); for (const rawLabel of rawLabels) { diff --git a/packages/opencensus-core/src/resource/types.ts b/packages/opencensus-core/src/resource/types.ts index d64742c4c..d32dba8ef 100644 --- a/packages/opencensus-core/src/resource/types.ts +++ b/packages/opencensus-core/src/resource/types.ts @@ -25,5 +25,8 @@ export interface Resource { * A dictionary of labels with string keys and values that provide information * about the entity. */ - readonly labels: {[key: string]: string;}; + readonly labels: Labels; } + +/** Labels are maps of keys -> values */ +export interface Labels { [key: string]: string; } diff --git a/packages/opencensus-core/test/test-resource.ts b/packages/opencensus-core/test/test-resource.ts index 77e69e4e9..e216c96a1 100644 --- a/packages/opencensus-core/test/test-resource.ts +++ b/packages/opencensus-core/test/test-resource.ts @@ -21,10 +21,10 @@ process.env.OC_RESOURCE_LABELS = 'k8s.io/pod/name="pod-xyz-123",k8s.io/container/name="c1",k8s.io/namespace/name="default"'; import {CoreResource} from '../src/resource/resource'; -import {Resource} from '../src/resource/types'; +import {Resource, Labels} from '../src/resource/types'; describe('Resource()', () => { - afterEach(() => { + after(() => { delete process.env.OC_RESOURCE_TYPE; delete process.env.OC_RESOURCE_LABELS; }); @@ -32,7 +32,7 @@ describe('Resource()', () => { it('should return resource information from environment variables', () => { const resource = CoreResource.createFromEnvironmentVariables(); const actualLabels = resource.labels; - const expectedLabels: {[key: string]: string} = { + const expectedLabels: Labels = { 'k8s.io/container/name': '"c1"', 'k8s.io/namespace/name': '"default"', 'k8s.io/pod/name': '"pod-xyz-123"' @@ -54,7 +54,7 @@ describe('mergeResources()', () => { it('merge resources with default, resource1', () => { const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1]; const resource = CoreResource.mergeResources(resources); - const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; + const expectedLabels: Labels = {'a': '1', 'b': '2'}; assert.equal(resource.type, 't1'); assert.equal(Object.keys(resource.labels).length, 2); @@ -64,7 +64,7 @@ describe('mergeResources()', () => { it('merge resources with default, resource1, resource2 = null', () => { const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1, null]; const resource = CoreResource.mergeResources(resources); - const expectedLabels: {[key: string]: string} = {'a': '1', 'b': '2'}; + const expectedLabels: Labels = {'a': '1', 'b': '2'}; assert.equal(resource.type, 't1'); assert.equal(Object.keys(resource.labels).length, 2); @@ -74,8 +74,7 @@ describe('mergeResources()', () => { it('merge resources with default, resource1 = null, resource2', () => { const resources: Resource[] = [DEFAULT_RESOURCE, null, RESOURCE_2]; const resource = CoreResource.mergeResources(resources); - const expectedLabels: - {[key: string]: string} = {'a': '1', 'b': '3', 'c': '4'}; + const expectedLabels: Labels = {'a': '1', 'b': '3', 'c': '4'}; assert.equal(resource.type, 't2'); assert.equal(Object.keys(resource.labels).length, 3); @@ -85,8 +84,7 @@ describe('mergeResources()', () => { it('merge resources with default1, resource1, resource2', () => { const resources: Resource[] = [DEFAULT_RESOURCE_1, RESOURCE_1, RESOURCE_2]; const resource = CoreResource.mergeResources(resources); - const expectedLabels: - {[key: string]: string} = {'a': '100', 'b': '2', 'c': '4'}; + const expectedLabels: Labels = {'a': '100', 'b': '2', 'c': '4'}; assert.equal(resource.type, 'default'); assert.equal(Object.keys(resource.labels).length, 3); @@ -97,7 +95,7 @@ describe('mergeResources()', () => { () => { const resources: Resource[] = [DEFAULT_RESOURCE_1, undefined, undefined]; const resource = CoreResource.mergeResources(resources); - const expectedLabels: {[key: string]: string} = {'a': '100'}; + const expectedLabels: Labels = {'a': '100'}; assert.equal(resource.type, 'default'); assert.equal(Object.keys(resource.labels).length, 1);