From 91c0bdc345333e051a716121a876655f3acaf674 Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Tue, 30 Jul 2024 17:26:07 +0300 Subject: [PATCH] feat: use upstream detectors, update container detector (#925) * use upstream env detector * use upstream os detector * use upstream host detector * use upstream process detector * update container detector * fix options test * use kernel archives for centos mirror --- .github/workflows/ci.yml | 4 +- src/detectors/ContainerDetector.ts | 141 ++++++++++++++++++ src/detectors/DockerCGroupV1Detector.ts | 135 ------------------ src/detectors/EnvDetector.ts | 182 ------------------------ src/detectors/HostDetector.ts | 75 ---------- src/detectors/OSDetector.ts | 73 ---------- src/detectors/ProcessDetector.ts | 95 ------------- src/resource.ts | 24 ++-- test/options.test.ts | 61 ++++---- test/resource.test.ts | 79 +++++----- 10 files changed, 227 insertions(+), 642 deletions(-) create mode 100644 src/detectors/ContainerDetector.ts delete mode 100644 src/detectors/DockerCGroupV1Detector.ts delete mode 100644 src/detectors/EnvDetector.ts delete mode 100644 src/detectors/HostDetector.ts delete mode 100644 src/detectors/OSDetector.ts delete mode 100644 src/detectors/ProcessDetector.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d47ac1..dacbffb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,7 +230,7 @@ jobs: - name: Setup container run: | sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* - sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=https://archive.kernel.org/centos-vault|g' /etc/yum.repos.d/CentOS-* yum update -y ${{ matrix.cmd }} - name: Checkout @@ -258,7 +258,7 @@ jobs: - name: Setup container run: | sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* - sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=https://archive.kernel.org/centos-vault|g' /etc/yum.repos.d/CentOS-* yum update -y curl -sL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash - && yum install -y nodejs gcc-c++ make - name: Checkout diff --git a/src/detectors/ContainerDetector.ts b/src/detectors/ContainerDetector.ts new file mode 100644 index 00000000..9b9a6067 --- /dev/null +++ b/src/detectors/ContainerDetector.ts @@ -0,0 +1,141 @@ +/* + * Copyright Splunk Inc. + * + * 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 is based on a detector from OTel https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/detectors/node/opentelemetry-resource-detector-container + The implementation has been changed from async to synchronous along with reduced log level for diagnostics. +*/ + +/* + * 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 { + DetectorSync, + Resource, + ResourceDetectionConfig, +} from '@opentelemetry/resources'; + +import { SEMRESATTRS_CONTAINER_ID } from '@opentelemetry/semantic-conventions'; + +import * as fs from 'fs'; +import { diag } from '@opentelemetry/api'; + +export class ContainerDetector implements DetectorSync { + readonly CONTAINER_ID_LENGTH = 64; + readonly DEFAULT_CGROUP_V1_PATH = '/proc/self/cgroup'; + readonly DEFAULT_CGROUP_V2_PATH = '/proc/self/mountinfo'; + readonly UTF8_UNICODE = 'utf8'; + readonly HOSTNAME = 'hostname'; + + detect(_config?: ResourceDetectionConfig): Resource { + try { + const containerId = this._getContainerId(); + return !containerId + ? Resource.empty() + : new Resource({ + [SEMRESATTRS_CONTAINER_ID]: containerId, + }); + } catch (e) { + diag.debug( + 'Container Detector did not identify running inside a supported container, no container attributes will be added to resource: ', + e + ); + return Resource.empty(); + } + } + + private _getContainerIdV1() { + const rawData = fs.readFileSync( + this.DEFAULT_CGROUP_V1_PATH, + this.UTF8_UNICODE + ); + const splitData = rawData.trim().split('\n'); + for (const line of splitData) { + const lastSlashIdx = line.lastIndexOf('/'); + if (lastSlashIdx === -1) { + continue; + } + const lastSection = line.substring(lastSlashIdx + 1); + const colonIdx = lastSection.lastIndexOf(':'); + if (colonIdx !== -1) { + // since containerd v1.5.0+, containerId is divided by the last colon when the cgroupDriver is systemd: + // https://github.com/containerd/containerd/blob/release/1.5/pkg/cri/server/helpers_linux.go#L64 + return lastSection.substring(colonIdx + 1); + } else { + let startIdx = lastSection.lastIndexOf('-'); + let endIdx = lastSection.lastIndexOf('.'); + + startIdx = startIdx === -1 ? 0 : startIdx + 1; + if (endIdx === -1) { + endIdx = lastSection.length; + } + if (startIdx > endIdx) { + continue; + } + return lastSection.substring(startIdx, endIdx); + } + } + return undefined; + } + + private _getContainerIdV2() { + const rawData = fs.readFileSync( + this.DEFAULT_CGROUP_V2_PATH, + this.UTF8_UNICODE + ); + const str = rawData + .trim() + .split('\n') + .find((s) => s.includes(this.HOSTNAME)); + const containerIdStr = str + ?.split('/') + .find((s) => s.length === this.CONTAINER_ID_LENGTH); + return containerIdStr || ''; + } + + /* + cgroupv1 path would still exist in case of container running on v2 + but the cgroupv1 path would no longer have the container id and would + fallback on the cgroupv2 implementation. + */ + private _getContainerId(): string | undefined { + try { + return this._getContainerIdV1() || this._getContainerIdV2(); + } catch (e) { + if (e instanceof Error) { + const errorMessage = e.message; + diag.debug( + 'Container Detector failed to read the Container ID: ', + errorMessage + ); + } + } + return undefined; + } +} + +export const containerDetector = new ContainerDetector(); diff --git a/src/detectors/DockerCGroupV1Detector.ts b/src/detectors/DockerCGroupV1Detector.ts deleted file mode 100644 index 94a035fc..00000000 --- a/src/detectors/DockerCGroupV1Detector.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * 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 is based on a detector from OTel https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/ - We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead - when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream. -*/ - -/* - * 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 { Resource, ResourceDetectionConfig } from '@opentelemetry/resources'; - -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; - -import * as fs from 'fs'; -import { platform } from 'os'; -import { diag } from '@opentelemetry/api'; - -const isValidBase16String = (hexString: string) => { - for (let ch = 0; ch < hexString.length; ch++) { - const code = hexString.charCodeAt(ch); - if ( - (48 <= code && code <= 57) || - (97 <= code && code <= 102) || - (65 <= code && code <= 70) - ) { - continue; - } - return false; - } - return true; -}; - -export class DockerCGroupV1Detector { - public detect(_config?: ResourceDetectionConfig): Resource { - if (platform() !== 'linux') { - diag.debug('Docker CGROUP V1 Detector skipped: Not on linux'); - return Resource.empty(); - } - try { - const containerId = this._getContainerId(); - return !containerId - ? Resource.empty() - : new Resource({ - [SemanticResourceAttributes.CONTAINER_ID]: containerId, - }); - } catch (e) { - diag.info( - 'Docker CGROUP V1 Detector did not identify running inside a supported docker container, no docker attributes will be added to resource: ', - e - ); - return Resource.empty(); - } - } - - protected _getContainerId(): string | null { - try { - const rawData = fs.readFileSync('/proc/self/cgroup', 'utf8').trim(); - return this._parseFile(rawData); - } catch (e) { - if (e instanceof Error) { - const errorMessage = e.message; - diag.info( - 'Docker CGROUP V1 Detector failed to read the Container ID: ', - errorMessage - ); - } - } - return null; - } - - /* - This is very likely has false positives since it does not check for the ID length, - but is very robust in usually finding the right thing, and if not, finding some - identifier for differentiating between containers. - It also matches Java: https://github.com/open-telemetry/opentelemetry-java/commit/2cb461d4aef16f1ac1c5e67edc2fb41f90ed96a3#diff-ad68bc34d4da31a50709591d4b7735f88c008be7ed1fc325c6367dd9df033452 - */ - protected _parseFile(contents: string): string | null { - if (typeof contents !== 'string') { - return null; - } - for (const line of contents.split('\n')) { - const lastSlashIdx = line.lastIndexOf('/'); - if (lastSlashIdx < 0) { - return null; - } - - const lastSection = line.substring(lastSlashIdx + 1); - let startIdx = lastSection.lastIndexOf('-'); - let endIdx = lastSection.lastIndexOf('.'); - - startIdx = startIdx === -1 ? 0 : startIdx + 1; - if (endIdx === -1) { - endIdx = lastSection.length; - } - if (startIdx > endIdx) { - return null; - } - - const containerId = lastSection.substring(startIdx, endIdx); - if (containerId && isValidBase16String(containerId)) { - return containerId; - } - } - return null; - } -} - -export const dockerCGroupV1Detector = new DockerCGroupV1Detector(); diff --git a/src/detectors/EnvDetector.ts b/src/detectors/EnvDetector.ts deleted file mode 100644 index f14d5b4e..00000000 --- a/src/detectors/EnvDetector.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * 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 is based on https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/EnvDetector.ts - We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead - when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream. -*/ - -/* - * 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 { diag } from '@opentelemetry/api'; -import { getEnv } from '@opentelemetry/core'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { - Resource, - ResourceAttributes, - ResourceDetectionConfig, -} from '@opentelemetry/resources'; - -export class EnvDetector { - // Type, attribute keys, and attribute values should not exceed 256 characters. - private readonly _MAX_LENGTH = 255; - - // OTEL_RESOURCE_ATTRIBUTES is a comma-separated list of attributes. - private readonly _COMMA_SEPARATOR = ','; - - // OTEL_RESOURCE_ATTRIBUTES contains key value pair separated by '='. - private readonly _LABEL_KEY_VALUE_SPLITTER = '='; - - private readonly _ERROR_MESSAGE_INVALID_CHARS = - 'should be a ASCII string with a length greater than 0 and not exceed ' + - this._MAX_LENGTH + - ' characters.'; - - private readonly _ERROR_MESSAGE_INVALID_VALUE = - 'should be a ASCII string with a length not exceed ' + - this._MAX_LENGTH + - ' characters.'; - - /** - * Returns a {@link Resource} populated with attributes from the - * OTEL_RESOURCE_ATTRIBUTES environment variable. Note this is an async - * function to conform to the Detector interface. - * - * @param config The resource detection config - */ - public detect(_config?: ResourceDetectionConfig): Resource { - const attributes: ResourceAttributes = {}; - const env = getEnv(); - - const rawAttributes = env.OTEL_RESOURCE_ATTRIBUTES; - const serviceName = env.OTEL_SERVICE_NAME; - - if (rawAttributes) { - try { - const parsedAttributes = this._parseResourceAttributes(rawAttributes); - Object.assign(attributes, parsedAttributes); - } catch (e) { - diag.debug(`EnvDetector failed: ${e instanceof Error ? e.message : e}`); - } - } else { - diag.debug( - 'EnvDetector failed: Environment variable "OTEL_RESOURCE_ATTRIBUTES" is missing.' - ); - } - - if (serviceName) { - attributes[SemanticResourceAttributes.SERVICE_NAME] = serviceName; - } - - return new Resource(attributes); - } - - /** - * Creates an attribute map from the OTEL_RESOURCE_ATTRIBUTES environment - * variable. - * - * OTEL_RESOURCE_ATTRIBUTES: A comma-separated list of attributes describing - * the source in more detail, e.g. “key1=val1,key2=val2”. Domain names and - * paths are accepted as attribute keys. Values may be quoted or unquoted in - * general. If a value contains whitespaces, =, or " characters, it must - * always be quoted. - * - * @param rawEnvAttributes The resource attributes as a comma-seperated list - * of key/value pairs. - * @returns The sanitized resource attributes. - */ - private _parseResourceAttributes( - rawEnvAttributes?: string - ): ResourceAttributes { - if (!rawEnvAttributes) return {}; - - const attributes: ResourceAttributes = {}; - const rawAttributes: string[] = rawEnvAttributes.split( - this._COMMA_SEPARATOR, - -1 - ); - for (const rawAttribute of rawAttributes) { - const keyValuePair: string[] = rawAttribute.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 (!this._isValidAndNotEmpty(key)) { - throw new Error(`Attribute key ${this._ERROR_MESSAGE_INVALID_CHARS}`); - } - if (!this._isValid(value)) { - throw new Error(`Attribute value ${this._ERROR_MESSAGE_INVALID_VALUE}`); - } - attributes[key] = value; - } - return attributes; - } - - /** - * Determines whether the given String is a valid printable ASCII string with - * a length not exceed _MAX_LENGTH characters. - * - * @param str The String to be validated. - * @returns Whether the String is valid. - */ - private _isValid(name: string): boolean { - return name.length <= this._MAX_LENGTH && this._isPrintableString(name); - } - - private _isPrintableString(str: string): boolean { - for (let i = 0; i < str.length; i++) { - const ch: string = str.charAt(i); - if (ch <= ' ' || ch >= '~') { - return false; - } - } - return true; - } - - /** - * Determines whether the given String is a valid printable ASCII string with - * a length greater than 0 and not exceed _MAX_LENGTH characters. - * - * @param str The String to be validated. - * @returns Whether the String is valid and not empty. - */ - private _isValidAndNotEmpty(str: string): boolean { - return str.length > 0 && this._isValid(str); - } -} - -export const envDetector = new EnvDetector(); -export { EnvDetector as EnvResourceDetector }; diff --git a/src/detectors/HostDetector.ts b/src/detectors/HostDetector.ts deleted file mode 100644 index 74c3424b..00000000 --- a/src/detectors/HostDetector.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * 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 is based on a detector from OTel https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/ - We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead - when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream. -*/ - -/* - * 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 { arch, hostname } from 'os'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { - Resource, - ResourceAttributes, - ResourceDetectionConfig, -} from '@opentelemetry/resources'; - -/** - * HostDetector detects the resources related to the host current process is - * running on. Currently only non-cloud-based attributes are included. - */ -class HostDetector { - public detect(_config?: ResourceDetectionConfig): Resource { - const attributes: ResourceAttributes = { - [SemanticResourceAttributes.HOST_NAME]: hostname(), - [SemanticResourceAttributes.HOST_ARCH]: this._normalizeArch(arch()), - }; - return new Resource(attributes); - } - - private _normalizeArch(nodeArchString: string): string { - // Maps from https://nodejs.org/api/os.html#osarch to arch values in spec: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/host.md - switch (nodeArchString) { - case 'arm': - return 'arm32'; - case 'ppc': - return 'ppc32'; - case 'x64': - return 'amd64'; - default: - return nodeArchString; - } - } -} - -export const hostDetector = new HostDetector(); diff --git a/src/detectors/OSDetector.ts b/src/detectors/OSDetector.ts deleted file mode 100644 index 0cf4a521..00000000 --- a/src/detectors/OSDetector.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * 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 is based on a detector from OTel https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/ - We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead - when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream. -*/ - -/* - * 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 { platform, release } from 'os'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { - Resource, - ResourceAttributes, - ResourceDetectionConfig, -} from '@opentelemetry/resources'; - -/** - * OSDetector detects the resources related to the operating system (OS) on - * which the process represented by this resource is running. - */ -class OSDetector { - public detect(_config?: ResourceDetectionConfig): Resource { - const attributes: ResourceAttributes = { - [SemanticResourceAttributes.OS_TYPE]: this._normalizeType(platform()), - [SemanticResourceAttributes.OS_VERSION]: release(), - }; - return new Resource(attributes); - } - - private _normalizeType(nodePlatform: string): string { - // Maps from https://nodejs.org/api/os.html#osplatform to arch values in spec: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/os.md - switch (nodePlatform) { - case 'sunos': - return 'solaris'; - case 'win32': - return 'windows'; - default: - return nodePlatform; - } - } -} - -export const osDetector = new OSDetector(); diff --git a/src/detectors/ProcessDetector.ts b/src/detectors/ProcessDetector.ts deleted file mode 100644 index 88a23983..00000000 --- a/src/detectors/ProcessDetector.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * 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 is based on https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/ProcessDetector.ts - We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead - when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream. -*/ - -/* - * 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 { diag } from '@opentelemetry/api'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { - Resource, - ResourceAttributes, - ResourceDetectionConfig, -} from '@opentelemetry/resources'; - -/** - * ProcessDetector will be used to detect the resources related current process running - * and being instrumented from the NodeJS Process module. - */ -class ProcessDetector { - detect(config?: ResourceDetectionConfig): Resource { - // Skip if not in Node.js environment. - if (typeof process !== 'object') { - return Resource.empty(); - } - const processResource: ResourceAttributes = { - [SemanticResourceAttributes.PROCESS_PID]: process.pid, - [SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME]: process.title || '', - [SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: - process.versions.node, - [SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'nodejs', - }; - return this._getResourceAttributes(processResource, config); - } - /** - * Validates process resource attribute map from process varaibls - * - * @param processResource The unsantized resource attributes from process as key/value pairs. - * @param config: Config - * @returns The sanitized resource attributes. - */ - private _getResourceAttributes( - processResource: ResourceAttributes, - _config?: ResourceDetectionConfig - ) { - if ( - processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME] === - '' || - processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_PATH] === - '' || - processResource[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION] === '' - ) { - diag.debug( - 'ProcessDetector failed: Unable to find required process resources. ' - ); - return Resource.empty(); - } else { - return new Resource({ - ...processResource, - }); - } - } -} - -export const processDetector = new ProcessDetector(); diff --git a/src/resource.ts b/src/resource.ts index 557807d0..baf7a860 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -15,22 +15,24 @@ */ import { diag } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; +import { + Resource, + envDetectorSync, + hostDetectorSync, + osDetectorSync, + processDetectorSync, +} from '@opentelemetry/resources'; import { distroDetector } from './detectors/DistroDetector'; -import { dockerCGroupV1Detector } from './detectors/DockerCGroupV1Detector'; -import { envDetector } from './detectors/EnvDetector'; -import { hostDetector } from './detectors/HostDetector'; -import { osDetector } from './detectors/OSDetector'; -import { processDetector } from './detectors/ProcessDetector'; +import { containerDetector } from './detectors/ContainerDetector'; const detectors = [ distroDetector, - dockerCGroupV1Detector, - envDetector, - hostDetector, - osDetector, - processDetector, + containerDetector, + envDetectorSync, + hostDetectorSync, + osDetectorSync, + processDetectorSync, ]; export const detect = (): Resource => { diff --git a/test/options.test.ts b/test/options.test.ts index 7c4aadfa..2377ea87 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -18,12 +18,22 @@ import * as api from '@opentelemetry/api'; import { W3CBaggagePropagator } from '@opentelemetry/core'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { Resource } from '@opentelemetry/resources'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { + SEMRESATTRS_CONTAINER_ID, + SEMRESATTRS_HOST_ARCH, + SEMRESATTRS_HOST_NAME, + SEMRESATTRS_OS_TYPE, + SEMRESATTRS_OS_VERSION, + SEMRESATTRS_PROCESS_EXECUTABLE_NAME, + SEMRESATTRS_PROCESS_PID, + SEMRESATTRS_PROCESS_RUNTIME_NAME, + SEMRESATTRS_PROCESS_RUNTIME_VERSION, + SEMRESATTRS_SERVICE_NAME, +} from '@opentelemetry/semantic-conventions'; import { ConsoleSpanExporter, SimpleSpanProcessor, SpanExporter, - SpanProcessor, InMemorySpanExporter, } from '@opentelemetry/sdk-trace-base'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; @@ -39,7 +49,6 @@ import { _setDefaultOptions, allowedTracingOptions, defaultPropagatorFactory, - otlpSpanExporterFactory, defaultSpanProcessorFactory, Options, } from '../src/tracing/options'; @@ -104,33 +113,36 @@ describe('options', () => { const options = _setDefaultOptions(); assertVersion( - options.tracerConfig.resource.attributes['splunk.distro.version'] + options.tracerConfig.resource?.attributes['splunk.distro.version'] ); const expectedAttributes = new Set([ - SemanticResourceAttributes.HOST_ARCH, - SemanticResourceAttributes.HOST_NAME, - SemanticResourceAttributes.OS_TYPE, - SemanticResourceAttributes.OS_VERSION, - SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME, - SemanticResourceAttributes.PROCESS_PID, - SemanticResourceAttributes.PROCESS_RUNTIME_NAME, - SemanticResourceAttributes.PROCESS_RUNTIME_VERSION, + SEMRESATTRS_HOST_ARCH, + SEMRESATTRS_HOST_NAME, + SEMRESATTRS_OS_TYPE, + SEMRESATTRS_OS_VERSION, + SEMRESATTRS_PROCESS_EXECUTABLE_NAME, + SEMRESATTRS_PROCESS_PID, + SEMRESATTRS_PROCESS_RUNTIME_NAME, + SEMRESATTRS_PROCESS_RUNTIME_VERSION, 'splunk.distro.version', ]); expectedAttributes.forEach((processAttribute) => { assert( - options.tracerConfig.resource.attributes[processAttribute], + options.tracerConfig.resource?.attributes[processAttribute], `${processAttribute} missing` ); }); + assert.deepStrictEqual( + options.tracerConfig.resource?.attributes[SEMRESATTRS_SERVICE_NAME], + '@splunk/otel' + ); + // Container ID is checked in a different test, // this avoids collisions with stubbing fs methods. - delete options.tracerConfig.resource.attributes[ - SemanticResourceAttributes.CONTAINER_ID - ]; + delete options.tracerConfig.resource.attributes[SEMRESATTRS_CONTAINER_ID]; // resource attributes for process, host and os are different at each run, iterate through them, make sure they exist and then delete Object.keys(options.tracerConfig.resource.attributes) @@ -138,7 +150,7 @@ describe('options', () => { return expectedAttributes.has(attribute); }) .forEach((processAttribute) => { - delete options.tracerConfig.resource.attributes[processAttribute]; + delete options.tracerConfig.resource?.attributes[processAttribute]; }); assert.deepStrictEqual( @@ -159,11 +171,6 @@ describe('options', () => { assert.deepStrictEqual(options.accessToken, ''); assert.deepStrictEqual(options.serverTimingEnabled, true); assert.deepStrictEqual(options.instrumentations, []); - assert.deepStrictEqual(options.tracerConfig, { - resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: '@splunk/otel', - }), - }); assert.deepStrictEqual( options.spanProcessorFactory, @@ -201,15 +208,13 @@ describe('options', () => { ); const options = _setDefaultOptions(); assertContainerId( - options.tracerConfig.resource.attributes[ - SemanticResourceAttributes.CONTAINER_ID - ] + options.tracerConfig.resource?.attributes[SEMRESATTRS_CONTAINER_ID] ); }); }); it('accepts and applies configuration', () => { - const testInstrumentation = new TestInstrumentation('inst', '1.0'); + const testInstrumentation = new TestInstrumentation('inst', '1.0', {}); const idGenerator = new TestIdGenerator(); const options = _setDefaultOptions({ @@ -289,9 +294,7 @@ describe('options', () => { delete process.env.OTEL_RESOURCE_ATTRIBUTES; assert.deepStrictEqual( - options.tracerConfig.resource.attributes[ - SemanticResourceAttributes.SERVICE_NAME - ], + options.tracerConfig.resource?.attributes[SEMRESATTRS_SERVICE_NAME], 'foobar' ); }); diff --git a/test/resource.test.ts b/test/resource.test.ts index d6cd3271..aa262c94 100644 --- a/test/resource.test.ts +++ b/test/resource.test.ts @@ -16,58 +16,32 @@ import * as assert from 'assert'; -import * as otel from '@opentelemetry/api'; -import { EnvDetector } from '../src/detectors/EnvDetector'; -import { DockerCGroupV1Detector } from '../src/detectors/DockerCGroupV1Detector'; +import { ContainerDetector } from '../src/detectors/ContainerDetector'; import { detect } from '../src/resource'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; import * as utils from './utils'; +import { SEMRESATTRS_CONTAINER_ID } from '@opentelemetry/semantic-conventions'; describe('resource detector', () => { beforeEach(() => { utils.cleanEnvironment(); }); - describe('EnvDetector', () => { - it('ignores missing attributes', () => { - const resource = new EnvDetector().detect(); - assert.deepStrictEqual(resource.attributes, {}); - }); - - it('ignores wrongly formatted env string', () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = 'kkkkkkkkkkk'; - const resource = new EnvDetector().detect(); - assert.deepStrictEqual(resource.attributes, {}); - }); - - it('ignores missing attr keys', () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = '=v'; - const resource = new EnvDetector().detect(); - assert.deepStrictEqual(resource.attributes, {}); - }); + describe('ContainerDetector', () => { + let sandbox: sinon.SinonSandbox; - it('ignores unsupported value chars', () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = 'k2=␟'; - const resource = new EnvDetector().detect(); - assert.deepStrictEqual(resource.attributes, {}); + before(() => { + sandbox = sinon.createSandbox(); }); - it('parses properly formatted attributes', () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = - 'k=v,key1=val1,service.name=node-svc'; - const resource = new EnvDetector().detect(); - assert.deepStrictEqual(resource.attributes, { - k: 'v', - key1: 'val1', - 'service.name': 'node-svc', - }); + after(() => { + sandbox.restore(); }); - }); - describe('DockerCGroupV1Detector', () => { const invalidCases = [ '13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz', ]; - const expectedId = 'ac679f8a8319c8cf7d38e1adf263bc08d23'; const testCases = [ [ '13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.slice', @@ -108,22 +82,34 @@ describe('resource detector', () => { ]; it('parses all the known test cases correctly', () => { - const detector = new DockerCGroupV1Detector(); + const detector = new ContainerDetector(); testCases.forEach(([testCase, result]) => { - assert.equal(detector['_parseFile'](testCase), result); + const stub = sinon.stub(fs, 'readFileSync').returns(testCase); + assert.strictEqual( + detector.detect().attributes[SEMRESATTRS_CONTAINER_ID], + result + ); + stub.restore(); }); invalidCases.forEach(([testCase, result]) => { - assert.equal(detector['_parseFile'](testCase), null); + const stub = sinon.stub(fs, 'readFileSync').returns(testCase); + assert.strictEqual( + detector.detect().attributes[SEMRESATTRS_CONTAINER_ID], + undefined + ); + stub.restore(); }); }); }); describe('resource.detect', () => { it('catches resource attributes from the env', () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = 'k=v,service.name=node-svc'; + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'k=v,service.name=node-svc,x=a%20b'; const resource = detect(); assert.strictEqual(resource.attributes['k'], 'v'); + assert.strictEqual(resource.attributes['x'], 'a b'); assert.strictEqual(resource.attributes['service.name'], 'node-svc'); }); @@ -143,11 +129,24 @@ describe('resource detector', () => { for (const attributeName of [ 'process.executable.name', + 'process.executable.path', 'process.runtime.version', 'process.runtime.name', ]) { assert.strictEqual(typeof resource.attributes[attributeName], 'string'); } }); + + it('detects OS attributes', () => { + const resource = detect(); + assert.strictEqual(typeof resource.attributes['os.type'], 'string'); + assert.strictEqual(typeof resource.attributes['os.version'], 'string'); + }); + + it('catches host attributes', () => { + const resource = detect(); + assert.strictEqual(typeof resource.attributes['host.name'], 'string'); + assert.strictEqual(typeof resource.attributes['host.arch'], 'string'); + }); }); });