diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEcsDetector.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEcsDetector.ts index 45a5b74dab..6bd96ea9e8 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEcsDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEcsDetector.ts @@ -163,10 +163,8 @@ export class AwsEcsDetector implements ResourceDetector { ); const splitData = rawData.trim().split('\n'); for (const str of splitData) { - if (str.length > AwsEcsDetector.CONTAINER_ID_LENGTH) { - containerId = str.substring( - str.length - AwsEcsDetector.CONTAINER_ID_LENGTH - ); + containerId = this._extractContainerIdFromLine(str); + if (containerId) { break; } } @@ -176,6 +174,74 @@ export class AwsEcsDetector implements ResourceDetector { return containerId; } + /** + * Extract container ID from a cgroup line. + * Handles the new AWS ECS Fargate format: /ecs//- + * Returns the last segment after the final '/' which should be the complete container ID. + */ + private _extractContainerIdFromLine(line: string): string | undefined { + if (!line) { + return undefined; + } + + // Split by '/' and get the last segment + const segments = line.split('/'); + if (segments.length <= 1) { + // Fallback to original logic if no '/' found + if (line.length > AwsEcsDetector.CONTAINER_ID_LENGTH) { + return line.substring(line.length - AwsEcsDetector.CONTAINER_ID_LENGTH); + } + return undefined; + } + + let lastSegment = segments[segments.length - 1]; + + // Handle containerd v1.5.0+ format with systemd cgroup driver (e.g., ending with :cri-containerd:containerid) + const colonIndex = lastSegment.lastIndexOf(':'); + if (colonIndex !== -1) { + lastSegment = lastSegment.substring(colonIndex + 1); + } + + // Remove known prefixes if they exist + const prefixes = ['docker-', 'crio-', 'cri-containerd-']; + for (const prefix of prefixes) { + if (lastSegment.startsWith(prefix)) { + lastSegment = lastSegment.substring(prefix.length); + break; + } + } + + // Remove anything after the first period (like .scope) + if (lastSegment.includes('.')) { + lastSegment = lastSegment.split('.')[0]; + } + + // Basic validation: should not be empty and should have reasonable length + if (!lastSegment || lastSegment.length < 8) { + return undefined; + } + + // AWS ECS container IDs can be in various formats: + // 1. Pure hex strings: 'abcdef123456' + // 2. ECS format: 'taskId-containerId' + // 3. Mixed alphanumeric with hyphens + // We'll be more permissive and allow alphanumeric characters and hyphens + const containerIdPattern = /^[a-zA-Z0-9\-_]+$/; + + if (containerIdPattern.test(lastSegment)) { + return lastSegment; + } + + // If the pattern doesn't match but the segment looks reasonable, + // still try to return it (last resort for edge cases) + if (lastSegment.length >= 12 && lastSegment.length <= 128) { + diag.debug(`AwsEcsDetector: Using container ID with non-standard format: ${lastSegment}`); + return lastSegment; + } + + return undefined; + } + /** * Add metadata-v4-related resource attributes to `data` (in-place) */ diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEcsDetector.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEcsDetector.test.ts index 1258de46a6..7c171c093a 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEcsDetector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEcsDetector.test.ts @@ -453,3 +453,291 @@ describe('AwsEcsResourceDetector', () => { }); }); }); + +describe('AwsEcsDetector - Container ID extraction improvements', () => { + let readStub: sinon.SinonStub; + + beforeEach(() => { + process.env.ECS_CONTAINER_METADATA_URI_V4 = 'http://169.254.170.2/v4/test'; + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('New AWS ECS Fargate cgroup format support', () => { + it('should extract full container ID from new AWS ECS Fargate format', async () => { + const taskId = 'c23e5f76c09d438aa1824ca4058bdcab'; + const containerId = '1234567890abcdef'; + const cgroupData = `/ecs/${taskId}/${taskId}-${containerId}`; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(cgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: `${taskId}-${containerId}`, // Expected: full taskId-containerId, not truncated + }); + + nockScope.done(); + }); + + it('should extract container ID from long cgroup path without truncation', async () => { + // Simulate the actual issue where the path is longer than 64 chars + const longTaskId = 'abcdefgh12345678abcdefgh12345678abcdefgh12345678'; + const containerId = '1234567890abcdef'; + const cgroupData = `/ecs/${longTaskId}/${longTaskId}-${containerId}`; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(cgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: `${longTaskId}-${containerId}`, // Should get full ID, not truncated + }); + + nockScope.done(); + }); + + it('should handle multiple cgroup lines and pick the valid one', async () => { + const taskId = 'c23e5f76c09d438aa1824ca4058bdcab'; + const containerId = '1234567890abcdef'; + const cgroupData = [ + '12:memory:/ecs', + '11:cpu:/ecs/task-id', + `10:devices:/ecs/${taskId}/${taskId}-${containerId}`, + '9:freezer:/ecs' + ].join('\n'); + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(cgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: `${taskId}-${containerId}`, + }); + + nockScope.done(); + }); + }); + + describe('Edge cases and format variations', () => { + it('should handle containerd format with colon separators', async () => { + const taskId = 'c23e5f76c09d438aa1824ca4058bdcab'; + const containerId = '1234567890abcdef'; + const cgroupData = `0::/system.slice/containerd.service/kubepods-burstable-pod.slice:cri-containerd:${taskId}-${containerId}`; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(cgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: `${taskId}-${containerId}`, + }); + + nockScope.done(); + }); + + it('should handle docker prefix and scope suffix', async () => { + const taskId = 'c23e5f76c09d438aa1824ca4058bdcab'; + const containerId = '1234567890abcdef'; + const cgroupData = `/docker/docker-${taskId}-${containerId}.scope`; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(cgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: `${taskId}-${containerId}`, + }); + + nockScope.done(); + }); + + it('should return undefined for invalid container ID formats', async () => { + const invalidCgroupData = '/invalid/path/with/non-hex-characters!!!'; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(invalidCgroupData); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + // id should be undefined due to invalid format + }); + + nockScope.done(); + }); + }); + + describe('Backward compatibility', () => { + it('should fallback to original logic for legacy format', async () => { + // Test backward compatibility with existing 64-char format + const legacyContainerId = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'; + + sinon.stub(os, 'hostname').returns('test-hostname'); + readStub = sinon + .stub(AwsEcsDetector, 'readFileAsync' as any) + .resolves(legacyContainerId); + + // Mock the metadata requests + const nockScope = nock('http://169.254.170.2:80') + .persist(false) + .get('/v4/test') + .reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' }) + .get('/v4/test/task') + .reply(200, { + TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test', + Family: 'test-family', + Revision: '1', + Cluster: 'test-cluster', + LaunchType: 'FARGATE' + }); + + const resource = detectResources({ detectors: [awsEcsDetector] }); + await resource.waitForAsyncAttributes?.(); + + sinon.assert.calledOnce(readStub); + assert.ok(resource); + assertEcsResource(resource, {}); + assertContainerResource(resource, { + name: 'test-hostname', + id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm', // Last 64 chars + }); + + nockScope.done(); + }); + }); +}); +