ChildProcessManager logs all stderr as errors — AWS SDK v2 warnings
RCA: ChildProcessManager::setupEventHandlers | Child process stderr — ForgeClient.handleApiError
Overview#
What Happened#
2026-06-24 13:44 KST에 cupixworks-any-bimrevision-agent 가 BIM revision 비교 작업(bim_id=18044, src_revision_id=24593)을 처리하는 동안 child process(BimCompareProcess) 의 stderr 로 Autodesk Forge AECModelData.json 404 응답 스택트레이스가 그대로 출력되었다. parent 의 ChildProcessManager.setupEventHandlers 는 stderr 전체를 logger.error(...) 로 라인 단위로 받아쓰기 때문에 한 번의 Forge API 404가 수십 줄의 error 레벨 로그로 분해되어 노이즈로 잡혔다. BIM revision 잡 자체는 BimRevisionService::REVISE-END 에서 "status":"success" 로 종료되었다 — 사용자 영향 없음.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ForgeUtilsError |
| exception.message | Resource not found: file/urn%3Aadsk.fluent%3Afs.file%3Aautodesk-360-translation-storage-prod%2Fw3cEko2vSNuSmggy9xLMHQ%2F21%2Foutput%2F0%2FAECModelData.json |
| errorCode | API_NOT_FOUND |
| top_frame | @cupixapps/forge-utils/server-utils/forge-client.js:90:31 (ForgeClient.handleApiError) |
| runtime | Node.js child process (forked from BimCompareManager) |
| package | @cupixapps/forge-utils@10.98.0, @cupixapps/forge-agents@10.98.0 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad (BIM revision agent) | 3 (이 cluster) | 사용자 영향 없음 — 잡은 정상 종료 (status:success). 로그 노이즈만 발생 |
Timeline#
- 2026-06-24 13:44:07 KST —
BimRevisionService::REVISE-BEGIN | bim_id=18044, src_revision_id=24593 (V24), prev_revision_id=24273 (V23)— Forge URNurn:adsk.wipprod:fs.file:vf.w3cEko2vSNuSmggy9xLMHQ(version 20 ↔ 21) 비교 시작 - 2026-06-24 13:44:08 KST — child process stderr 에
ForgeUtilsError: Resource not found ... /21/output/0/AECModelData.json첫 발생 (first_seen) - 2026-06-24 13:44:10 KST — version 20 / version 21 양쪽에 대해 동일한 404 한 번 더 발생 (
last_seen, 총 3건) - 2026-06-24 13:46:57 KST — 별개의
bim_id=20016revision 비교 시작 (관련 없는 후속 잡) - 2026-06-24 13:48:18 KST —
BimRevisionService::REVISE-END | bim_id=18044 ... "status":"success", "elapsed_ms":253285— 404 발생에도 잡은 success 종료
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: at ForgeClient.handleApiError (/tmp/agent/dist/node_modules/.pnpm/@cupixapps+forge-utils@10.98.0_axios@1.16.1_commander@3.0.2_fflate@0.8.3_rxjs@7.8.2_three@0.170.0_ws@8.21.0/node_modules/@cupixapps/forge-utils/server-utils/forge-client.js:90:31)
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: gad
- 발생 횟수: 3
- 최초 발생: 2026-06-24 13:44 KST
- 최근 발생: 2026-06-24 13:44 KST
- 사용자 영향: 없음.
BimRevisionService::REVISE-END가status:"success"로 종료됨. Compare 결과modified=0, removed=0, exist=0으로 정상 처리.
Root Cause Summary#
근본 원인은 child process 의 stderr 처리 방식이다. BimCompareManager 는 BIM 비교 작업을 위해 ChildProcessManager 로 child process 를 fork 하고, 부모는 child_process.manager.ts:138-141 에서 process.stderr.on('data') 에 들어오는 모든 buffer chunk 를 logger.error(...) 로 흘려보낸다. 그런데 child process 안에서 동작하는 외부 패키지 @cupixapps/forge-utils@10.98.0 의 ForgeClient.handleApiError (forge-client.js:90) 가 Autodesk Forge Model Derivative API 에서 404 (AECModelData.json not found) 를 받았을 때 에러를 그대로 throw 하면 Node 가 unhandled rejection 또는 catch 후 console 출력 형태로 stderr 에 stack trace 를 newline 단위로 뱉는다. 부모 쪽 stderr.on('data') 는 chunk 단위로 잘려 들어오므로 stack 한 줄(예: at ForgeClient.handleApiError ...) 이 단독 chunk 로 도착해 fingerprint 매칭 시 이 한 줄이 cluster 의 representative error 가 되었다. 한편 child 의 내부 catch 가 Compare 단계에서 404 를 swallow 하기 때문에 BimRevisionService 레벨에서는 잡이 success 로 종료된다. 즉, 이 cluster 는 외부 Forge API 의 404 가 parent 의 stderr passthrough 로 인해 status:error 로 잘못 분류된 “로그 노이즈” 다.
Technical Analysis#
Code Path#
Parent 쪽 stderr forwarding:
this.process.stderr?.on('data', (data: Buffer) => {
const text = data.toString();
logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});
stdioMode: 'pipe' 로 fork 한 경우(BIM revision 은 native C++ 라이브러리 호출 가능성 때문에 pipe 사용) child 가 stderr 에 쓰는 모든 라인이 그대로 logger.error 로 흘러간다. 청크는 줄 단위로 분리되지 않고 OS buffer 단위로 잘려 들어오기 때문에 한 stack trace 가 여러 error 로그로 분할된다.
Child process 내 호출 흐름 (top_frame 인 ForgeClient.handleApiError 까지):
private async execute(params: BimComparerParams): Promise<ForgeAgent.Result> {
try {
this.log(`BimCompareProcess::execute | forge-agents version: ${forgeAgentsPkg.version}, spec_version: ${SpecVersion}`);
this.log('BimCompareProcess::execute | start');
const result: ForgeAgent.Result = await ForgeAgent.extract(params.apiConfig, params.urn, {
region: params.region,
extractRoom: false,
extractMeta: false,
extractCompare: {
urn: params.previousUrn,
region: params.previousRegion,
query: params.query
}
});
...
} catch (error) {
...
throw error;
}
}
ForgeAgent.extract 내부에서 prev/current 두 URN 의 SVF2 manifest(AECModelData.json) 를 받아오려고 ForgeClient.getBuffer → ForgeClient.handleApiError 를 호출한다. 404 시 handleApiError 가 ForgeUtilsError 를 throw 하지만, 호출 체인 어딘가 (compare-extractor.js, grid-alignment.js) 에서 이 에러를 catch 하기 때문에 child process 는 정상 종료되고 BimCompareProcess::execute 의 result 도 받게 된다. 다만 catch 전에 axios/Node 가 stack trace 를 stderr 로 인쇄하는 시점이 있어 parent 가 그것을 잡아 error 레벨로 기록한다.
Parent의 BimCompareManager 가 에러를 catch 하면 BimRevisionExtractorExecute 라는 errorCode 가 호출자에 전달되어야 하지만, 실제로는 child 가 결과를 정상 반환했기 때문에 setErrorCode 도 호출되지 않는다:
async execute(params: BimComparerParams): Promise<ForgeAgent.Result> {
...
try {
const result = await this.childProcessManager.execute<ForgeAgent.Result>('execute', params);
logger.debug('BimCompareManager::execute | completed successfully');
return result;
} catch (error) {
logger.error('BimCompareManager::execute | error:', error);
const errorCode = ErrorCode.Agent.BimRevisionExtractorExecute;
if (this.setErrorCode) {
this.setErrorCode(errorCode);
}
throw error;
}
}
Failure point 는 코드의 버그가 아니라 로깅 정책이다: child 의 stderr 가 정상 처리되는 (recoverable) 에러임에도 무조건 logger.error 로 분류된다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-any-bimrevision-agent status:error "ChildProcessManager::setupEventHandlers"
Time window: 2026-06-24T04:30:00Z ~ 2026-06-24T05:00:00Z.
핵심 stderr 라인 — AECModelData.json 404 (version 21):
ChildProcessManager::setupEventHandlers | Child process stderr: ForgeUtilsError: Resource not found: file/urn%3Aadsk.fluent%3Afs.file%3Aautodesk-360-translation-storage-prod%2Fw3cEko2vSNuSmggy9xLMHQ%2F21%2Foutput%2F0%2FAECModelData.json?acmsession=dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLnczY0VrbzJ2U051U21nZ3k5eExNSFE_dmVyc2lvbj0yMQ
ChildProcessManager::setupEventHandlers | Child process stderr: at ForgeClient.handleApiError (/tmp/agent/dist/node_modules/.pnpm/@cupixapps+forge-utils@10.98.0_.../forge-client.js:90:31)
ChildProcessManager::setupEventHandlers | Child process stderr: at ModelDataClient.getBuffer (.../model-data.js:144:24)
ChildProcessManager::setupEventHandlers | Child process stderr: at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
ChildProcessManager::setupEventHandlers | Child process stderr: at async ModelDataClient.getData (.../model-data.js:144:24)
ChildProcessManager::setupEventHandlers | Child process stderr: at async Asset.get (.../core-utils/block.js:18:32)
ChildProcessManager::setupEventHandlers | Child process stderr: at async BIMSVF2Manifest.AEC (.../svf2-manifest.js:90:25)
ChildProcessManager::setupEventHandlers | Child process stderr: at async GridAlignment.computeGridCorrection (.../grid-alignment.js:25:27)
ChildProcessManager::setupEventHandlers | Child process stderr: at async Object.calculateOrigin (.../compare-utils.js:76:36)
ChildProcessManager::setupEventHandlers | Child process stderr: at async CompareExtractor.compare (.../compare-extractor.js:62:43)
ChildProcessManager::setupEventHandlers | Child process stderr: at async extractComparison (.../forge-agents/app.js:133:20) {
ChildProcessManager::setupEventHandlers | Child process stderr: errorCode: 'API_NOT_FOUND'
ChildProcessManager::setupEventHandlers | Child process stderr: }
같은 stack 이 version 20 (prev_forge_urn) 에 대해서도 한 번 더 반복된다.
잡 자체는 success 종료 — 사용자 영향 없음:
{
"bim_id": 18044,
"src_revision_id": 24593,
"prev_revision_id": 24273,
"si_trace_id": "676699f8-68e5-40ed-867a-4d3ee79f29a9",
"elapsed_ms": 253285,
"status": "success",
"counts": { "total": 0, "modified": 0, "removed": 0, "exist": 0 }
}
REVISE-BEGIN 로그가 동일한 URN 을 가리키는 것을 확인:
{
"bim_id": 18044,
"src_revision_id": 24593,
"src_revision_name": "V24",
"prev_revision_id": 24273,
"prev_revision_name": "V23",
"agent_version": "10.98.0",
"src_forge_urn": "dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLnczY0VrbzJ2U051U21nZ3k5eExNSFE_dmVyc2lvbj0yMQ==",
"prev_forge_urn": "dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLnczY0VrbzJ2U051U21nZ3k5eExNSFE_dmVyc2lvbj0yMA=="
}
src_forge_urn (base64 decode) = urn:adsk.wipprod:fs.file:vf.w3cEko2vSNuSmggy9xLMHQ?version=21 — stderr 의 404 URL 과 정확히 일치.
추가로 같은 시간대 stderr 라인들 중에는 Forge 와 무관한 AWS SDK v2 deprecation warning 도 status:error 로 잡혀 있는데, 같은 메커니즘이다:
ChildProcessManager::setupEventHandlers | Child process stderr: (node:45) NOTE: The AWS SDK for JavaScript (v2) will enter maintenance mode
ChildProcessManager::setupEventHandlers | Child process stderr: on September 8, 2024 and reach end-of-support on September 8, 2025.
ChildProcessManager::setupEventHandlers | Child process stderr: Please migrate your code to use AWS SDK for JavaScript (v3).
이는 child 의 stderr 가 error 레벨로 매핑되는 정책의 부작용을 추가로 보여준다 — Node 의 --trace-warnings deprecation notice 까지 error 로그가 된다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | child process 의 stderr 가 무조건 logger.error 로 흐르기 때문에, child 내부에서 recoverable 한 404 (AECModelData.json API_NOT_FOUND) 가 parent 의 error 로그로 분류된다 |
child-process.manager.ts:138-141 가 모든 stderr chunk 를 logger.error 로 보냄 / 동일 시간 window 에 REVISE-END "status":"success", elapsed_ms:253285 가 기록됨 / AWS SDK v2 deprecation 노이즈까지 같은 형식으로 error 로 잡힘 |
— | Confirmed |
| H2 | BIM revision 잡이 실제로 실패해서 사용자 영향이 발생한다 | stack trace 가 extractComparison → CompareExtractor.compare 호출 경로에서 발생 |
BimRevisionService::REVISE-END 가 status:"success" 로 종료 (Datadog 13:48:18 KST). Compare 결과 정상 반환 (modified/removed/exist 모두 0) |
Rejected |
| H3 | BimCompareManager.execute 가 errorCode BimRevisionExtractorExecute 를 set 해서 호출자에게 전달했다 |
bim-compare.manager.ts:71 가 catch 에서 setErrorCode 호출 |
child IPC 가 result 를 정상 반환했으므로 parent 의 await this.childProcessManager.execute(...) 가 throw 하지 않음 → catch 블록 자체가 실행되지 않음. REVISE-END status:success 와 일관 |
Rejected |
| H4 | Forge Model Derivative 일시 outage 가 root cause | errorCode: 'API_NOT_FOUND' 가 한 객체에 대해서만 발생 |
다른 URN(bim_id=20016) revision 은 같은 시간대에 success 로 종료. 404 가 일시적이 아니라 그 특정 translation output 에 AECModelData.json 이 만들어지지 않은 영구 상태로 보임 |
Rejected |
| H5 | 동일 scope (svc:cupixworks-any-bimrevision-agent::unknown) 의 대량 발생 incident 가 별개의 root cause |
status-board 가 2026-06-24-svc-cupixworks-any-bimrevision-agent--unknown-1 에 20개 cluster 를 묶어둠 |
묶인 cluster 모두 같은 stderr passthrough 메커니즘에서 파생 (서로 다른 stack frame 이 분리된 청크로 도착해서 다른 fingerprint 가 됨) | Rejected — 같은 root cause |
Fix Recommendation#
즉시 조치 (Critical)#
없음. 사용자 영향이 없고 잡은 정상 종료된다. 운영 alarming 만 조정한다 — 이 fingerprint 로 페이지를 울리지 않도록 monitor 에서 제외하거나 status-board 에서 dismiss 처리.
단기 개선 (1주 이내)#
applications/agents/packages/base/src/manager/child-process.manager.ts:138-141의 stderr passthrough 정책을 재검토.- 방향: child 의 stderr 를 무조건
logger.error로 흘리지 말고logger.warn(또는info) 로 다운그레이드하거나, 라인 패턴으로 분류하라. 예:errorCode: 'API_NOT_FOUND'또는node: NOTE:같은 deprecation prefix 는warn, 실제 fatal 만error. 단, parent 가 child 의 정상 종료 여부(closeevent 의code) 를 따로 보고 있으므로 stderr 자체는 진단 정보로 충분히 다뤄도 안전. - 근거: 같은 코드 경로(
logger.error) 가 deprecation warning, recoverable 404, 진짜 crash 를 모두 같은 레벨로 묶고 있어 alert/triage 가 무의미해진다.
- 방향: child 의 stderr 를 무조건
BimCompareProcess::execute(bim-compare.process.ts:67-115) 의 catch 가error.message/error.stack을this.log로 IPC 통해 parent 로 보내는 구조와, 외부 패키지가 stderr 에 직접 인쇄하는 구조가 중복되어 있음 — child 안에서console.error(또는 axios 의 default error 출력) 를 silence 하고 IPC 채널로만 보고하도록 정리.
장기 개선 (재발 방지)#
- classifier 측:
Child process stderr:prefix 가 붙은 multi-line stack chunk 를 하나의 fingerprint 로 정규화하도록 룰 추가. 현재는 한 trace 가 ≥10 cluster 로 폭발한다 (status-board 의 incident2026-06-24-svc-cupixworks-any-bimrevision-agent--unknown-1에 20 cluster). @cupixapps/forge-utils가 던지는API_NOT_FOUND를 BIM revision agent 가 일급 도메인 신호로 받아서, "이 translation 에는AECModelData.json이 없음 → grid alignment skip" 같은 식으로 명시적으로 처리하고 비-에러 로그로 기록하는 PR. 그러면 stderr 자체에도 stack 이 안 찍힌다.
Monitoring#
-
추가/조정 모니터링:
-
bim-revision agent 실제 실패율 (REVISE-END status != success). 이 cluster 류 노이즈에 의존하지 않는 정확한 사용자 영향 지표:
textsum:logs.hits\{service:cupixworks-any-bimrevision-agent,@evt.name:BimRevisionService.REVISE-END,@status:failure\}.as_count() -
child process stderr 노이즈 양 (트렌드 확인용):
textsum:logs.hits\{service:cupixworks-any-bimrevision-agent,status:error,@message:"Child process stderr*"\}.as_count() -
Forge API_NOT_FOUND 빈도 (도메인 신호로 추적):
textsum:logs.hits\{service:cupixworks-any-bimrevision-agent,status:error,@message:"API_NOT_FOUND"\}.as_count()
-
-
Datadog 검색 쿼리 (재현용):
textservice:cupixworks-any-bimrevision-agent status:error "ChildProcessManager::setupEventHandlers" "ForgeUtilsError"
Risk Assessment#
- Risk level: low (사용자 영향 없음, REVISE-END success)
- 예상 복잡도: standard (외부 패키지 영향 없이 parent 측 로깅 정책만 조정해도 노이즈 제거 가능)