Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f3db796
fix(detector-aws): extract full container ID from ECS Fargate cgroup …
seongpil0948 Jul 12, 2025
f2be3bf
test(detector-aws): refactor ECS container ID extraction tests
seongpil0948 Jul 20, 2025
a406a64
fix(detector-aws): extract full container ID from ECS Fargate cgroup …
seongpil0948 Jul 25, 2025
01469cc
chore(comment): remove comment
seongpil0948 Jul 26, 2025
2ee2f08
Merge branch 'main' into fix/2455_ECS-consistent-container-id
seongpil0948 Aug 8, 2025
1f31841
feat: Apply suggestions from code review
seongpil0948 Aug 8, 2025
75e3471
feat(aws-ecs-detector): improve container ID validation and test cove…
seongpil0948 Aug 9, 2025
6eecd69
Merge branch 'open-telemetry:main' into fix/2455_ECS-consistent-conta…
seongpil0948 Aug 12, 2025
7c19509
feat(resource-detector-aws): prefer primary container ID across lines…
seongpil0948 Aug 15, 2025
7df9ad2
Merge branch 'open-telemetry:main' into fix/2455_ECS-consistent-conta…
seongpil0948 Aug 15, 2025
2f37644
Merge branch 'main' into fix/2455_ECS-consistent-container-id
seongpil0948 Aug 21, 2025
5505284
fix(resource-detector-aws): remove redundant variable in ECS containe…
seongpil0948 Aug 21, 2025
6337a9e
Merge branch 'main' into fix/2455_ECS-consistent-container-id
seongpil0948 Aug 24, 2025
c8add07
Merge branch 'main' into fix/2455_ECS-consistent-container-id
seongpil0948 Aug 29, 2025
098ef50
fix: lint
pichlermarc Aug 29, 2025
b509c16
Merge branch 'main' into fix/2455_ECS-consistent-container-id
pichlermarc Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions packages/resource-detector-aws/src/detectors/AwsEcsDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface AwsLogOptions {
*/
export class AwsEcsDetector implements ResourceDetector {
static readonly CONTAINER_ID_LENGTH = 64;
static readonly CONTAINER_ID_LENGTH_MIN = 32;
static readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup';

private static readFileAsync = util.promisify(fs.readFile);
Expand Down Expand Up @@ -155,25 +156,76 @@ export class AwsEcsDetector implements ResourceDetector {
* and then return undefined.
*/
private async _getContainerId(): Promise<string | undefined> {
let containerId = undefined;
try {
const rawData = await AwsEcsDetector.readFileAsync(
AwsEcsDetector.DEFAULT_CGROUP_PATH,
'utf8'
);
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
);
break;
}
const lines = rawData
.split('\n')
.map(s => s.trim())
.filter(Boolean);

// Pass 1: Prefer primary ECS pattern across all lines
for (const line of lines) {
const id = this._extractPrimaryEcsContainerId(line);
if (id) return id;
}

// Pass 2: Fallback to last-segment with strict allowed chars (hex + hyphen)
for (const line of lines) {
const id = this._extractLastSegmentContainerId(line);
if (id) return id;
}

// Pass 3: Legacy fallback to last 64 chars (Docker-style), keep existing behavior
for (const line of lines) {
const id = this._extractLegacyContainerId(line);
if (id) return id;
}
} catch (e) {
diag.debug('AwsEcsDetector failed to read container ID', e);
}
return containerId;
return undefined;
}

// Prefer primary ECS format extraction
private _extractPrimaryEcsContainerId(line: string): string | undefined {
const ecsPattern = /\/ecs\/[a-fA-F0-9-]+\/([a-fA-F0-9-]+)$/;
const match = line.match(ecsPattern);
if (
match &&
match[1] &&
match[1].length >= AwsEcsDetector.CONTAINER_ID_LENGTH_MIN &&
match[1].length <= AwsEcsDetector.CONTAINER_ID_LENGTH
) {
return match[1];
}
return undefined;
}

// Fallback: accept last path segment if it looks like a container id (hex + '-')
private _extractLastSegmentContainerId(line: string): string | undefined {
const parts = line.split('/');
if (parts.length <= 1) return undefined;
const last = parts[parts.length - 1];
if (
last &&
last.length >= AwsEcsDetector.CONTAINER_ID_LENGTH_MIN &&
last.length <= AwsEcsDetector.CONTAINER_ID_LENGTH &&
/^[a-fA-F0-9-]+$/.test(last)
) {
return last;
}
return undefined;
}

// Legacy fallback: keep existing behavior to avoid breaking users/tests
private _extractLegacyContainerId(line: string): string | undefined {
if (line.length > AwsEcsDetector.CONTAINER_ID_LENGTH) {
return line.substring(line.length - AwsEcsDetector.CONTAINER_ID_LENGTH);
}
return undefined;
}

/**
Expand Down
137 changes: 137 additions & 0 deletions packages/resource-detector-aws/test/detectors/AwsEcsDetector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,4 +452,141 @@ describe('AwsEcsResourceDetector', () => {
});
});
});

describe('Container ID extraction', () => {
const testMetadataUri = 'http://169.254.170.2/v4/test';
const testHostname = 'test-hostname';
let readStub: sinon.SinonStub;

beforeEach(() => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = testMetadataUri;
});

afterEach(() => {
sinon.restore();
});

function setupMocks(cgroupData: string) {
sinon.stub(os, 'hostname').returns(testHostname);
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);
}

function setupMetadataNock() {
return 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',
});
}

it('should extract full container ID from new ECS Fargate format', async () => {
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
const containerId = '1234567890abcdef';
const cgroupData = `/ecs/${taskId}/${taskId}-${containerId}`;

setupMocks(cgroupData);
const nockScope = setupMetadataNock();

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: testHostname,
id: `${taskId}-${containerId}`,
});

nockScope.done();
});

it('should handle backward compatibility with legacy format', async () => {
const legacyContainerId =
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm';

setupMocks(legacyContainerId);
const nockScope = setupMetadataNock();

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: testHostname,
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
});

nockScope.done();
});

it('should extract container ID from Docker format cgroup', async () => {
const dockerContainerId =
'a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856';
const cgroupData = `1:blkio:/docker/${dockerContainerId}
2:cpu:/docker/${dockerContainerId}
3:cpuacct:/docker/${dockerContainerId}`;

setupMocks(cgroupData);
const nockScope = setupMetadataNock();

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: testHostname,
id: dockerContainerId,
});

nockScope.done();
});

it('should extract container ID from mixed ECS and Docker format cgroup', async () => {
const taskId = '447438d8540d49dca93b4f0a488ebe90';
const containerId = `${taskId}-1364044452`;
const cgroupData = `11:memory:/ecs/${taskId}/${containerId}
10:devices:/ecs/${taskId}/${containerId}
9:freezer:/ecs/${taskId}/${containerId}
8:blkio:/ecs/${taskId}/${containerId}
7:perf_event:/ecs/${taskId}/${containerId}
6:net_cls,net_prio:/ecs/${taskId}/${containerId}
5:cpuset:/ecs/${taskId}/${containerId}
4:pids:/ecs/${taskId}/${containerId}
3:hugetlb:/ecs/${taskId}/${containerId}
2:cpu,cpuacct:/ecs/${taskId}/${containerId}
1:name=systemd:/ecs/${taskId}/${containerId}`;

setupMocks(cgroupData);
const nockScope = setupMetadataNock();

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: testHostname,
id: containerId,
});

nockScope.done();
});
});
});
Loading