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..966a8e66a --- /dev/null +++ b/packages/opencensus-core/src/internal/string-utils.ts @@ -0,0 +1,47 @@ +/** + * 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. + * @returns {boolean} 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; + } + + /** + * Determines whether the Character is printable. + * + * @param {string} str The Character to be validated. + * @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 new file mode 100644 index 000000000..03e4c3acf --- /dev/null +++ b/packages/opencensus-core/src/resource/resource.ts @@ -0,0 +1,178 @@ +/** + * 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'; + +import {Labels, Resource} from './types'; + +/** + * 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 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. + 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 = + CoreResource.parseResourceType(process.env.OC_RESOURCE_TYPE); + private static readonly ENV_LABEL_MAP = + 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 ' + + CoreResource.MAX_LENGTH + ' characters.'; + private static readonly ERROR_MESSAGE_INVALID_VALUE = + 'should be a ASCII string with a length not exceed ' + + 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} The resource. + */ + static createFromEnvironmentVariables(): Resource { + return {type: CoreResource.ENV_TYPE, labels: CoreResource.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} The resource. + */ + static mergeResources(resources: Resource[]): Resource { + let currentResource: Resource; + for (const resource of resources) { + currentResource = this.merge(currentResource, resource); + } + return currentResource; + } + + /** + * 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 (!CoreResource.isValidAndNotEmpty(rawEnvType)) { + throw new Error(`Type ${CoreResource.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. + * + * @param {string} rawEnvLabels The resource labels as a comma-seperated list + * of key/value pairs. + * @returns {Labels} The sanitized resource labels. + */ + 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) { + const keyValuePair: string[] = + rawLabel.split(this.LABEL_KEY_VALUE_SPLITTER, -1); + if (keyValuePair.length !== 2) { + continue; + } + let [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 (!CoreResource.isValid(value)) { + throw new Error( + `Label value ${CoreResource.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. + * + * @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) { + return otherResource; + } + if (!otherResource) { + return resource; + } + 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 <= 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 && CoreResource.isValid(name); + } +} diff --git a/packages/opencensus-core/src/resource/types.ts b/packages/opencensus-core/src/resource/types.ts new file mode 100644 index 000000000..d32dba8ef --- /dev/null +++ b/packages/opencensus-core/src/resource/types.ts @@ -0,0 +1,32 @@ +/** + * 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; + + /** + * A dictionary of labels with string keys and values that provide information + * about the entity. + */ + 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 new file mode 100644 index 000000000..e216c96a1 --- /dev/null +++ b/packages/opencensus-core/test/test-resource.ts @@ -0,0 +1,104 @@ +/** + * 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 {CoreResource} from '../src/resource/resource'; +import {Resource, Labels} from '../src/resource/types'; + +describe('Resource()', () => { + after(() => { + delete process.env.OC_RESOURCE_TYPE; + delete process.env.OC_RESOURCE_LABELS; + }); + + it('should return resource information from environment variables', () => { + const resource = CoreResource.createFromEnvironmentVariables(); + const actualLabels = resource.labels; + const expectedLabels: Labels = { + 'k8s.io/container/name': '"c1"', + 'k8s.io/namespace/name': '"default"', + 'k8s.io/pod/name': '"pod-xyz-123"' + }; + + assert.strictEqual(resource.type, 'k8s.io/container'); + assert.equal(Object.keys(actualLabels).length, 3); + assert.deepEqual(actualLabels, expectedLabels); + }); +}); + +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); + const expectedLabels: Labels = {'a': '1', 'b': '2'}; + + 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 = CoreResource.mergeResources(resources); + const expectedLabels: Labels = {'a': '1', 'b': '2'}; + + 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 = CoreResource.mergeResources(resources); + const expectedLabels: Labels = {'a': '1', 'b': '3', 'c': '4'}; + + 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 = CoreResource.mergeResources(resources); + const expectedLabels: Labels = {'a': '100', 'b': '2', 'c': '4'}; + + 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 = CoreResource.mergeResources(resources); + const expectedLabels: Labels = {'a': '100'}; + + assert.equal(resource.type, 'default'); + assert.equal(Object.keys(resource.labels).length, 1); + assert.deepEqual(resource.labels, 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); + }); +});