Skip to content

Commit c1866e1

Browse files
seongpil0948claudejj22eepichlermarc
authored
fix(detector-aws): extract full container ID from ECS Fargate cgroup (#2855)
Co-authored-by: Claude <[email protected]> Co-authored-by: Jonathan Lee <[email protected]> Co-authored-by: Marc Pichler <[email protected]>
1 parent 0b6a515 commit c1866e1

File tree

2 files changed

+199
-10
lines changed

2 files changed

+199
-10
lines changed

packages/resource-detector-aws/src/detectors/AwsEcsDetector.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface AwsLogOptions {
6363
*/
6464
export class AwsEcsDetector implements ResourceDetector {
6565
static readonly CONTAINER_ID_LENGTH = 64;
66+
static readonly CONTAINER_ID_LENGTH_MIN = 32;
6667
static readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup';
6768

6869
private static readFileAsync = util.promisify(fs.readFile);
@@ -155,25 +156,76 @@ export class AwsEcsDetector implements ResourceDetector {
155156
* and then return undefined.
156157
*/
157158
private async _getContainerId(): Promise<string | undefined> {
158-
let containerId = undefined;
159159
try {
160160
const rawData = await AwsEcsDetector.readFileAsync(
161161
AwsEcsDetector.DEFAULT_CGROUP_PATH,
162162
'utf8'
163163
);
164-
const splitData = rawData.trim().split('\n');
165-
for (const str of splitData) {
166-
if (str.length > AwsEcsDetector.CONTAINER_ID_LENGTH) {
167-
containerId = str.substring(
168-
str.length - AwsEcsDetector.CONTAINER_ID_LENGTH
169-
);
170-
break;
171-
}
164+
const lines = rawData
165+
.split('\n')
166+
.map(s => s.trim())
167+
.filter(Boolean);
168+
169+
// Pass 1: Prefer primary ECS pattern across all lines
170+
for (const line of lines) {
171+
const id = this._extractPrimaryEcsContainerId(line);
172+
if (id) return id;
173+
}
174+
175+
// Pass 2: Fallback to last-segment with strict allowed chars (hex + hyphen)
176+
for (const line of lines) {
177+
const id = this._extractLastSegmentContainerId(line);
178+
if (id) return id;
179+
}
180+
181+
// Pass 3: Legacy fallback to last 64 chars (Docker-style), keep existing behavior
182+
for (const line of lines) {
183+
const id = this._extractLegacyContainerId(line);
184+
if (id) return id;
172185
}
173186
} catch (e) {
174187
diag.debug('AwsEcsDetector failed to read container ID', e);
175188
}
176-
return containerId;
189+
return undefined;
190+
}
191+
192+
// Prefer primary ECS format extraction
193+
private _extractPrimaryEcsContainerId(line: string): string | undefined {
194+
const ecsPattern = /\/ecs\/[a-fA-F0-9-]+\/([a-fA-F0-9-]+)$/;
195+
const match = line.match(ecsPattern);
196+
if (
197+
match &&
198+
match[1] &&
199+
match[1].length >= AwsEcsDetector.CONTAINER_ID_LENGTH_MIN &&
200+
match[1].length <= AwsEcsDetector.CONTAINER_ID_LENGTH
201+
) {
202+
return match[1];
203+
}
204+
return undefined;
205+
}
206+
207+
// Fallback: accept last path segment if it looks like a container id (hex + '-')
208+
private _extractLastSegmentContainerId(line: string): string | undefined {
209+
const parts = line.split('/');
210+
if (parts.length <= 1) return undefined;
211+
const last = parts[parts.length - 1];
212+
if (
213+
last &&
214+
last.length >= AwsEcsDetector.CONTAINER_ID_LENGTH_MIN &&
215+
last.length <= AwsEcsDetector.CONTAINER_ID_LENGTH &&
216+
/^[a-fA-F0-9-]+$/.test(last)
217+
) {
218+
return last;
219+
}
220+
return undefined;
221+
}
222+
223+
// Legacy fallback: keep existing behavior to avoid breaking users/tests
224+
private _extractLegacyContainerId(line: string): string | undefined {
225+
if (line.length > AwsEcsDetector.CONTAINER_ID_LENGTH) {
226+
return line.substring(line.length - AwsEcsDetector.CONTAINER_ID_LENGTH);
227+
}
228+
return undefined;
177229
}
178230

179231
/**

packages/resource-detector-aws/test/detectors/AwsEcsDetector.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,4 +452,141 @@ describe('AwsEcsResourceDetector', () => {
452452
});
453453
});
454454
});
455+
456+
describe('Container ID extraction', () => {
457+
const testMetadataUri = 'http://169.254.170.2/v4/test';
458+
const testHostname = 'test-hostname';
459+
let readStub: sinon.SinonStub;
460+
461+
beforeEach(() => {
462+
process.env.ECS_CONTAINER_METADATA_URI_V4 = testMetadataUri;
463+
});
464+
465+
afterEach(() => {
466+
sinon.restore();
467+
});
468+
469+
function setupMocks(cgroupData: string) {
470+
sinon.stub(os, 'hostname').returns(testHostname);
471+
readStub = sinon
472+
.stub(AwsEcsDetector, 'readFileAsync' as any)
473+
.resolves(cgroupData);
474+
}
475+
476+
function setupMetadataNock() {
477+
return nock('http://169.254.170.2:80')
478+
.persist(false)
479+
.get('/v4/test')
480+
.reply(200, {
481+
ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test',
482+
})
483+
.get('/v4/test/task')
484+
.reply(200, {
485+
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
486+
Family: 'test-family',
487+
Revision: '1',
488+
Cluster: 'test-cluster',
489+
LaunchType: 'FARGATE',
490+
});
491+
}
492+
493+
it('should extract full container ID from new ECS Fargate format', async () => {
494+
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
495+
const containerId = '1234567890abcdef';
496+
const cgroupData = `/ecs/${taskId}/${taskId}-${containerId}`;
497+
498+
setupMocks(cgroupData);
499+
const nockScope = setupMetadataNock();
500+
501+
const resource = detectResources({ detectors: [awsEcsDetector] });
502+
await resource.waitForAsyncAttributes?.();
503+
504+
sinon.assert.calledOnce(readStub);
505+
assert.ok(resource);
506+
assertEcsResource(resource, {});
507+
assertContainerResource(resource, {
508+
name: testHostname,
509+
id: `${taskId}-${containerId}`,
510+
});
511+
512+
nockScope.done();
513+
});
514+
515+
it('should handle backward compatibility with legacy format', async () => {
516+
const legacyContainerId =
517+
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm';
518+
519+
setupMocks(legacyContainerId);
520+
const nockScope = setupMetadataNock();
521+
522+
const resource = detectResources({ detectors: [awsEcsDetector] });
523+
await resource.waitForAsyncAttributes?.();
524+
525+
sinon.assert.calledOnce(readStub);
526+
assert.ok(resource);
527+
assertEcsResource(resource, {});
528+
assertContainerResource(resource, {
529+
name: testHostname,
530+
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
531+
});
532+
533+
nockScope.done();
534+
});
535+
536+
it('should extract container ID from Docker format cgroup', async () => {
537+
const dockerContainerId =
538+
'a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856';
539+
const cgroupData = `1:blkio:/docker/${dockerContainerId}
540+
2:cpu:/docker/${dockerContainerId}
541+
3:cpuacct:/docker/${dockerContainerId}`;
542+
543+
setupMocks(cgroupData);
544+
const nockScope = setupMetadataNock();
545+
546+
const resource = detectResources({ detectors: [awsEcsDetector] });
547+
await resource.waitForAsyncAttributes?.();
548+
549+
sinon.assert.calledOnce(readStub);
550+
assert.ok(resource);
551+
assertEcsResource(resource, {});
552+
assertContainerResource(resource, {
553+
name: testHostname,
554+
id: dockerContainerId,
555+
});
556+
557+
nockScope.done();
558+
});
559+
560+
it('should extract container ID from mixed ECS and Docker format cgroup', async () => {
561+
const taskId = '447438d8540d49dca93b4f0a488ebe90';
562+
const containerId = `${taskId}-1364044452`;
563+
const cgroupData = `11:memory:/ecs/${taskId}/${containerId}
564+
10:devices:/ecs/${taskId}/${containerId}
565+
9:freezer:/ecs/${taskId}/${containerId}
566+
8:blkio:/ecs/${taskId}/${containerId}
567+
7:perf_event:/ecs/${taskId}/${containerId}
568+
6:net_cls,net_prio:/ecs/${taskId}/${containerId}
569+
5:cpuset:/ecs/${taskId}/${containerId}
570+
4:pids:/ecs/${taskId}/${containerId}
571+
3:hugetlb:/ecs/${taskId}/${containerId}
572+
2:cpu,cpuacct:/ecs/${taskId}/${containerId}
573+
1:name=systemd:/ecs/${taskId}/${containerId}`;
574+
575+
setupMocks(cgroupData);
576+
const nockScope = setupMetadataNock();
577+
578+
const resource = detectResources({ detectors: [awsEcsDetector] });
579+
await resource.waitForAsyncAttributes?.();
580+
581+
sinon.assert.calledOnce(readStub);
582+
assert.ok(resource);
583+
assertEcsResource(resource, {});
584+
assertContainerResource(resource, {
585+
name: testHostname,
586+
id: containerId,
587+
});
588+
589+
nockScope.done();
590+
});
591+
});
455592
});

0 commit comments

Comments
 (0)