diff --git a/detectors/node/opentelemetry-resource-detector-container/src/detectors/ContainerDetector.ts b/detectors/node/opentelemetry-resource-detector-container/src/detectors/ContainerDetector.ts index 78bffb7572..ff204d8087 100644 --- a/detectors/node/opentelemetry-resource-detector-container/src/detectors/ContainerDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-container/src/detectors/ContainerDetector.ts @@ -24,6 +24,7 @@ import { SEMRESATTRS_CONTAINER_ID } from '@opentelemetry/semantic-conventions'; import * as fs from 'fs'; import * as util from 'util'; import { diag } from '@opentelemetry/api'; +import { extractContainerIdFromLine } from './utils'; export class ContainerDetector implements Detector { readonly CONTAINER_ID_LENGTH = 64; @@ -31,6 +32,11 @@ export class ContainerDetector implements Detector { readonly DEFAULT_CGROUP_V2_PATH = '/proc/self/mountinfo'; readonly UTF8_UNICODE = 'utf8'; readonly HOSTNAME = 'hostname'; + readonly MARKING_PREFIX = 'containers'; + readonly CRIO = 'crio-'; + readonly CRI_CONTAINERD = 'cri-containerd-'; + readonly DOCKER = 'docker-'; + readonly HEX_STRING_REGEX: RegExp = /^[a-f0-9]+$/i; private static readFileAsync = util.promisify(fs.readFile); @@ -51,35 +57,17 @@ export class ContainerDetector implements Detector { } } - private async _getContainerIdV1() { + private async _getContainerIdV1(): Promise { const rawData = await ContainerDetector.readFileAsync( 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); + for (const line of splitData) { + const containerID = extractContainerIdFromLine(line); + if (containerID) { + return containerID; } } return undefined; @@ -94,10 +82,19 @@ export class ContainerDetector implements Detector { .trim() .split('\n') .find(s => s.includes(this.HOSTNAME)); - const containerIdStr = str - ?.split('/') - .find(s => s.length === this.CONTAINER_ID_LENGTH); - return containerIdStr || ''; + + if (!str) return ''; + + const strArray = str?.split('/') ?? []; + for (let i = 0; i < strArray.length - 1; i++) { + if ( + strArray[i] === this.MARKING_PREFIX && + strArray[i + 1]?.length === this.CONTAINER_ID_LENGTH + ) { + return strArray[i + 1]; + } + } + return ''; } /* @@ -107,9 +104,14 @@ export class ContainerDetector implements Detector { */ private async _getContainerId(): Promise { try { - return ( - (await this._getContainerIdV1()) || (await this._getContainerIdV2()) - ); + const containerIdV1 = await this._getContainerIdV1(); + if (containerIdV1) { + return containerIdV1; // If containerIdV1 is a non-empty string, return it. + } + const containerIdV2 = await this._getContainerIdV2(); + if (containerIdV2) { + return containerIdV2; // If containerIdV2 is a non-empty string, return it. + } } catch (e) { if (e instanceof Error) { const errorMessage = e.message; @@ -119,7 +121,7 @@ export class ContainerDetector implements Detector { ); } } - return undefined; + return undefined; // Explicitly return undefined if neither ID is found. } } diff --git a/detectors/node/opentelemetry-resource-detector-container/src/detectors/utils.ts b/detectors/node/opentelemetry-resource-detector-container/src/detectors/utils.ts new file mode 100644 index 0000000000..96bae1fe80 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-container/src/detectors/utils.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ +export const CONTAINER_ID_LENGTH = 64; +export const DEFAULT_CGROUP_V1_PATH = '/proc/self/cgroup'; +export const DEFAULT_CGROUP_V2_PATH = '/proc/self/mountinfo'; +export const UTF8_UNICODE = 'utf8'; +export const HOSTNAME = 'hostname'; +export const MARKING_PREFIX = 'containers'; +export const CRIO = 'crio-'; +export const CRI_CONTAINERD = 'cri-containerd-'; +export const DOCKER = 'docker-'; +export const HEX_STRING_REGEX = /^[a-f0-9]+$/i; + +export function truncatePrefix(lastSection: string, prefix: string): string { + return lastSection.substring(prefix.length); +} + +export function extractContainerIdFromLine(line: string): string | undefined { + if (!line) { + return undefined; + } + const sections = line.split('/'); + if (sections.length <= 1) { + return undefined; + } + let lastSection = sections[sections.length - 1]; + + // Handle containerd v1.5.0+ format with systemd cgroup driver + const colonIndex = lastSection.lastIndexOf(':'); + if (colonIndex !== -1) { + lastSection = lastSection.substring(colonIndex + 1); + } + + // Truncate known prefixes from the last section + if (lastSection.startsWith(CRIO)) { + lastSection = truncatePrefix(lastSection, CRIO); + } else if (lastSection.startsWith(DOCKER)) { + lastSection = truncatePrefix(lastSection, DOCKER); + } else if (lastSection.startsWith(CRI_CONTAINERD)) { + lastSection = truncatePrefix(lastSection, CRI_CONTAINERD); + } + // Remove anything after the first period + if (lastSection.includes('.')) { + lastSection = lastSection.split('.')[0]; + } + // Check if the remaining string is a valid hex string + if (HEX_STRING_REGEX.test(lastSection)) { + return lastSection; + } + return undefined; +} diff --git a/detectors/node/opentelemetry-resource-detector-container/test/ContainerDetector.test.ts b/detectors/node/opentelemetry-resource-detector-container/test/ContainerDetector.test.ts index 71fbf1c5de..e39748f994 100644 --- a/detectors/node/opentelemetry-resource-detector-container/test/ContainerDetector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-container/test/ContainerDetector.test.ts @@ -26,10 +26,10 @@ import { import { ContainerDetector } from '../src'; describe('ContainerDetector', () => { - let readStub; + let readStub: sinon.SinonStub; const correctCgroupV1Data = - '12:pids:/kubepods.slice/bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'; - const correctCgroupV2Data = `tmhdefghijklmnopqrstuvwxyzafgrefghiugkmnopqrstuvwxyzabcdefghijkl/hostname + '12:pids:/kubepods.slice/4e6f77206973207468652074696d6520666f7220616c6c20676f6f64206d656e20746f20636f6d6520746f2074686520616964'; + const correctCgroupV2Data = `containers/tmhdefghijklmnopqrstuvwxyzafgrefghiugkmnopqrstuvwxyzabcdefghijkl/hostname fhkjdshgfhsdfjhdsfkjhfkdshkjhfd/host sahfhfjkhjhfhjdhfjkdhfkjdhfjkhhdsjfhdfhjdhfkj/somethingelse`; @@ -63,7 +63,7 @@ describe('ContainerDetector', () => { assert.ok(resource); assertContainerResource(resource, { - id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm', + id: '4e6f77206973207468652074696d6520666f7220616c6c20676f6f64206d656e20746f20636f6d6520746f2074686520616964', }); }); diff --git a/detectors/node/opentelemetry-resource-detector-container/test/utils.test.ts b/detectors/node/opentelemetry-resource-detector-container/test/utils.test.ts new file mode 100644 index 0000000000..193feb6fa3 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-container/test/utils.test.ts @@ -0,0 +1,91 @@ +/* + * 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 * as assert from 'assert'; +import { extractContainerIdFromLine } from '../src/detectors/utils'; + +describe(' extractContainerId from line tests', () => { + it('should extract container ID from crio-prefixed line', () => { + const line = + '11:devices:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod5c5979ec_6b2b_11e9_a923_42010a800002.slice/crio-1234567890abcdef.scope'; + const expected = '1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); + + it('should extract container ID from docker-prefixed line', () => { + const line = + '11:devices:/docker/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const expected = + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); + + it('should extract container ID from cri-containerd-prefixed line', () => { + const line = + '11:devices:/kubepods/burstable/pod2c4b2241-5c01-11e9-8e4e-42010a800002/cri-containerd-1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const expected = + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); + + it('should handle containerd v1.5.0+ format with systemd cgroup driver', () => { + const line = + '0::/system.slice/containerd.service/kubepods-burstable-pod2c4b2241-5c01-11e9-8e4e-42010a800002.slice:cri-containerd:1234567890abcdef'; + const expected = '1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); + + it('should return undefined for invalid container ID', () => { + const line = + '11:devices:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod5c5979ec_6b2b_11e9_a923_42010a800002.slice/invalid-id.scope'; + assert.strictEqual(extractContainerIdFromLine(line), undefined); + }); + + it('should return undefined for empty line', () => { + const line = ''; + assert.strictEqual(extractContainerIdFromLine(line), undefined); + }); + + it('should return undefined for line without container ID', () => { + const line = '11:devices:/'; + assert.strictEqual(extractContainerIdFromLine(line), undefined); + }); + + // Additional test cases + it('should handle line with multiple colons', () => { + const line = + '0::/system.slice/containerd.service/kubepods-burstable-pod2c4b2241-5c01-11e9-8e4e-42010a800002.slice:cri-containerd-1234567890abcdef.extra'; + const expected = '1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); + + it('should return containerid for valid hex string with any length', () => { + const line = + '11:devices:/docker/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde'; + assert.strictEqual( + extractContainerIdFromLine(line), + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde' + ); + }); + + it('should extract container ID with additional suffix', () => { + const line = + '11:devices:/docker/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef.suffix'; + const expected = + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + assert.strictEqual(extractContainerIdFromLine(line), expected); + }); +});