Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 5fe3b38

Browse files
authored
Add Resource API (#193)
* Add Resource API * Fix reviews * Add interface: Resource * fix reviews * remove null from Resource: type * Add interface for Labels
1 parent 765bae3 commit 5fe3b38

5 files changed

Lines changed: 391 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Internal utility methods for working with tag keys, tag values, and metric
19+
* names.
20+
*/
21+
export class StringUtils {
22+
/**
23+
* Determines whether the String contains only printable characters.
24+
*
25+
* @param {string} str The String to be validated.
26+
* @returns {boolean} Whether the String contains only printable characters.
27+
*/
28+
static isPrintableString(str: string): boolean {
29+
for (let i = 0; i < str.length; i++) {
30+
const ch: string = str.charAt(i);
31+
if (!StringUtils.isPrintableChar(ch)) {
32+
return false;
33+
}
34+
}
35+
return true;
36+
}
37+
38+
/**
39+
* Determines whether the Character is printable.
40+
*
41+
* @param {string} str The Character to be validated.
42+
* @returns {boolean} Whether the Character is printable.
43+
*/
44+
static isPrintableChar(ch: string): boolean {
45+
return ch >= ' ' && ch <= '~';
46+
}
47+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {StringUtils} from '../internal/string-utils';
18+
19+
import {Labels, Resource} from './types';
20+
21+
/**
22+
* Resource represents a resource, which capture identifying information about
23+
* the entities for which signals (stats or traces) are reported. It further
24+
* provides a framework for detection of resource information from the
25+
* environment and progressive population as signals propagate from the core
26+
* instrumentation library to a backend's exporter.
27+
*/
28+
export class CoreResource {
29+
// Type, label keys, and label values should not exceed 256 characters.
30+
private static readonly MAX_LENGTH = 255;
31+
32+
// OC_RESOURCE_LABELS is a comma-separated list of labels.
33+
private static readonly COMMA_SEPARATOR = ',';
34+
35+
// OC_RESOURCE_LABELS contains key value pair separated by '='.
36+
private static readonly LABEL_KEY_VALUE_SPLITTER = '=';
37+
38+
private static readonly ENV_TYPE =
39+
CoreResource.parseResourceType(process.env.OC_RESOURCE_TYPE);
40+
private static readonly ENV_LABEL_MAP =
41+
CoreResource.parseResourceLabels(process.env.OC_RESOURCE_LABELS);
42+
private static readonly ERROR_MESSAGE_INVALID_CHARS =
43+
'should be a ASCII string with a length greater than 0 and not exceed ' +
44+
CoreResource.MAX_LENGTH + ' characters.';
45+
private static readonly ERROR_MESSAGE_INVALID_VALUE =
46+
'should be a ASCII string with a length not exceed ' +
47+
CoreResource.MAX_LENGTH + ' characters.';
48+
49+
/**
50+
* Returns a Resource. This resource information is loaded from the
51+
* OC_RESOURCE_TYPE and OC_RESOURCE_LABELS environment variables.
52+
*
53+
* @returns {Resource} The resource.
54+
*/
55+
static createFromEnvironmentVariables(): Resource {
56+
return {type: CoreResource.ENV_TYPE, labels: CoreResource.ENV_LABEL_MAP};
57+
}
58+
59+
/**
60+
* Returns a Resource that runs all input resources sequentially and merges
61+
* their results. In case a type of label key is already set, the first set
62+
* value takes precedence.
63+
*
64+
* @param {Resource[]} resources The list of the resources.
65+
* @returns {Resource} The resource.
66+
*/
67+
static mergeResources(resources: Resource[]): Resource {
68+
let currentResource: Resource;
69+
for (const resource of resources) {
70+
currentResource = this.merge(currentResource, resource);
71+
}
72+
return currentResource;
73+
}
74+
75+
/**
76+
* Creates a resource type from the OC_RESOURCE_TYPE environment variable.
77+
*
78+
* OC_RESOURCE_TYPE: A string that describes the type of the resource
79+
* prefixed by a domain namespace, e.g. “kubernetes.io/container”.
80+
*
81+
* @param {string} rawEnvType The resource type.
82+
* @returns {string} The sanitized resource type.
83+
*/
84+
private static parseResourceType(rawEnvType: string): string {
85+
if (rawEnvType) {
86+
if (!CoreResource.isValidAndNotEmpty(rawEnvType)) {
87+
throw new Error(`Type ${CoreResource.ERROR_MESSAGE_INVALID_CHARS}`);
88+
}
89+
return rawEnvType.trim();
90+
}
91+
return null;
92+
}
93+
94+
/**
95+
* Creates a label map from the OC_RESOURCE_LABELS environment variable.
96+
*
97+
* OC_RESOURCE_LABELS: A comma-separated list of labels describing the
98+
* source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths
99+
* are accepted as label keys. Values may be quoted or unquoted in general. If
100+
* a value contains whitespaces, =, or " characters, it must always be quoted.
101+
*
102+
* @param {string} rawEnvLabels The resource labels as a comma-seperated list
103+
* of key/value pairs.
104+
* @returns {Labels} The sanitized resource labels.
105+
*/
106+
private static parseResourceLabels(rawEnvLabels: string): Labels {
107+
const labels: Labels = {};
108+
if (rawEnvLabels) {
109+
const rawLabels: string[] = rawEnvLabels.split(this.COMMA_SEPARATOR, -1);
110+
for (const rawLabel of rawLabels) {
111+
const keyValuePair: string[] =
112+
rawLabel.split(this.LABEL_KEY_VALUE_SPLITTER, -1);
113+
if (keyValuePair.length !== 2) {
114+
continue;
115+
}
116+
let [key, value] = keyValuePair;
117+
// Leading and trailing whitespaces are trimmed.
118+
key = key.trim();
119+
value = value.trim().split('^"|"$').join('');
120+
if (!CoreResource.isValidAndNotEmpty(key)) {
121+
throw new Error(
122+
`Label key ${CoreResource.ERROR_MESSAGE_INVALID_CHARS}`);
123+
}
124+
if (!CoreResource.isValid(value)) {
125+
throw new Error(
126+
`Label value ${CoreResource.ERROR_MESSAGE_INVALID_VALUE}`);
127+
}
128+
labels[key] = value;
129+
}
130+
}
131+
return labels;
132+
}
133+
134+
/**
135+
* Returns a new, merged Resource by merging two resources. In case of
136+
* a collision, first resource takes precedence.
137+
*
138+
* @param {Resource} resource The resource object.
139+
* @param {Resource} otherResource The resource object.
140+
* @returns {Resource} A new, merged Resource.
141+
*/
142+
private static merge(resource: Resource, otherResource: Resource): Resource {
143+
if (!resource) {
144+
return otherResource;
145+
}
146+
if (!otherResource) {
147+
return resource;
148+
}
149+
return {
150+
type: resource.type || otherResource.type,
151+
labels: Object.assign({}, otherResource.labels, resource.labels)
152+
};
153+
}
154+
155+
156+
/**
157+
* Determines whether the given String is a valid printable ASCII string with
158+
* a length not exceed MAX_LENGTH characters.
159+
*
160+
* @param {string} str The String to be validated.
161+
* @returns {boolean} Whether the String is valid.
162+
*/
163+
private static isValid(name: string): boolean {
164+
return name.length <= CoreResource.MAX_LENGTH &&
165+
StringUtils.isPrintableString(name);
166+
}
167+
168+
/**
169+
* Determines whether the given String is a valid printable ASCII string with
170+
* a length greater than 0 and not exceed MAX_LENGTH characters.
171+
*
172+
* @param {string} str The String to be validated.
173+
* @returns {boolean} Whether the String is valid and not empty.
174+
*/
175+
private static isValidAndNotEmpty(name: string): boolean {
176+
return name && name.length > 0 && CoreResource.isValid(name);
177+
}
178+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** A Resource describes the entity for which a signal was collected. */
18+
export interface Resource {
19+
/**
20+
* An optional string which describes a well-known type of resource.
21+
*/
22+
readonly type: string;
23+
24+
/**
25+
* A dictionary of labels with string keys and values that provide information
26+
* about the entity.
27+
*/
28+
readonly labels: Labels;
29+
}
30+
31+
/** Labels are maps of keys -> values */
32+
export interface Labels { [key: string]: string; }
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
19+
process.env.OC_RESOURCE_TYPE = 'k8s.io/container';
20+
process.env.OC_RESOURCE_LABELS =
21+
'k8s.io/pod/name="pod-xyz-123",k8s.io/container/name="c1",k8s.io/namespace/name="default"';
22+
23+
import {CoreResource} from '../src/resource/resource';
24+
import {Resource, Labels} from '../src/resource/types';
25+
26+
describe('Resource()', () => {
27+
after(() => {
28+
delete process.env.OC_RESOURCE_TYPE;
29+
delete process.env.OC_RESOURCE_LABELS;
30+
});
31+
32+
it('should return resource information from environment variables', () => {
33+
const resource = CoreResource.createFromEnvironmentVariables();
34+
const actualLabels = resource.labels;
35+
const expectedLabels: Labels = {
36+
'k8s.io/container/name': '"c1"',
37+
'k8s.io/namespace/name': '"default"',
38+
'k8s.io/pod/name': '"pod-xyz-123"'
39+
};
40+
41+
assert.strictEqual(resource.type, 'k8s.io/container');
42+
assert.equal(Object.keys(actualLabels).length, 3);
43+
assert.deepEqual(actualLabels, expectedLabels);
44+
});
45+
});
46+
47+
describe('mergeResources()', () => {
48+
const DEFAULT_RESOURCE: Resource = {type: null, labels: {}};
49+
const DEFAULT_RESOURCE_1: Resource = {type: 'default', labels: {'a': '100'}};
50+
const RESOURCE_1: Resource = {type: 't1', labels: {'a': '1', 'b': '2'}};
51+
const RESOURCE_2:
52+
Resource = {type: 't2', labels: {'a': '1', 'b': '3', 'c': '4'}};
53+
54+
it('merge resources with default, resource1', () => {
55+
const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1];
56+
const resource = CoreResource.mergeResources(resources);
57+
const expectedLabels: Labels = {'a': '1', 'b': '2'};
58+
59+
assert.equal(resource.type, 't1');
60+
assert.equal(Object.keys(resource.labels).length, 2);
61+
assert.deepEqual(resource.labels, expectedLabels);
62+
});
63+
64+
it('merge resources with default, resource1, resource2 = null', () => {
65+
const resources: Resource[] = [DEFAULT_RESOURCE, RESOURCE_1, null];
66+
const resource = CoreResource.mergeResources(resources);
67+
const expectedLabels: Labels = {'a': '1', 'b': '2'};
68+
69+
assert.equal(resource.type, 't1');
70+
assert.equal(Object.keys(resource.labels).length, 2);
71+
assert.deepEqual(resource.labels, expectedLabels);
72+
});
73+
74+
it('merge resources with default, resource1 = null, resource2', () => {
75+
const resources: Resource[] = [DEFAULT_RESOURCE, null, RESOURCE_2];
76+
const resource = CoreResource.mergeResources(resources);
77+
const expectedLabels: Labels = {'a': '1', 'b': '3', 'c': '4'};
78+
79+
assert.equal(resource.type, 't2');
80+
assert.equal(Object.keys(resource.labels).length, 3);
81+
assert.deepEqual(resource.labels, expectedLabels);
82+
});
83+
84+
it('merge resources with default1, resource1, resource2', () => {
85+
const resources: Resource[] = [DEFAULT_RESOURCE_1, RESOURCE_1, RESOURCE_2];
86+
const resource = CoreResource.mergeResources(resources);
87+
const expectedLabels: Labels = {'a': '100', 'b': '2', 'c': '4'};
88+
89+
assert.equal(resource.type, 'default');
90+
assert.equal(Object.keys(resource.labels).length, 3);
91+
assert.deepEqual(resource.labels, expectedLabels);
92+
});
93+
94+
it('merge resources with default, resource1 = undefined, resource2 = undefined',
95+
() => {
96+
const resources: Resource[] = [DEFAULT_RESOURCE_1, undefined, undefined];
97+
const resource = CoreResource.mergeResources(resources);
98+
const expectedLabels: Labels = {'a': '100'};
99+
100+
assert.equal(resource.type, 'default');
101+
assert.equal(Object.keys(resource.labels).length, 1);
102+
assert.deepEqual(resource.labels, expectedLabels);
103+
});
104+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import {StringUtils} from '../src/internal/string-utils';
19+
20+
describe('StringUtils', () => {
21+
it('should return true when string is printable', () => {
22+
const isValid = StringUtils.isPrintableString('abcd');
23+
assert.deepStrictEqual(isValid, true);
24+
});
25+
26+
it('should return false when string is not printable', () => {
27+
const isValid = StringUtils.isPrintableString('\x00-\xFF');
28+
assert.deepStrictEqual(isValid, false);
29+
});
30+
});

0 commit comments

Comments
 (0)