fail to post to issue service. reason: 'RestClient error' message: {"result":{"code":"ARG10002","mes
RCA: fail to post to issue service - IssueType not found (ARG10002)
Error Log#
fail to post to issue service. reason: 'RestClient error' message: {"result":{"code":"ARG10002","message":"IssueType not found"}}
Impact#
- Service:
cupixvista-api-worker - 발생 횟수: 34
- 최초 발생: 2026-04-06T10:35:10.075Z
- 최근 발생: 2026-04-07T07:12:58.298Z
Root Cause Summary#
cupixvista-api-worker의 IssueServiceWorker가 캡처 처리 실패 시 issue-service(cupixworks 모노레포의 Lambda)에 이슈 생성 요청을 보내는데, IssueType 매핑이 없는 error_code에 대해서도 요청이 전송되고 있다. IssueType이 모든 error_code에 존재하는 것이 아니라, 일부 error_code에만 매핑이 존재하는 것이 정상 설계이다 (seed.json 기준 AGT10004, ARG10000만 IssueType 매핑 보유). 문제는 두 곳에 있다:
- issue-service (
create.ts:79-81): IssueType을 찾지 못하면 무조건 HTTP 400ARG10002를 반환하는 validation이 과도함. IssueType 없이도 이슈 생성을 허용하거나 graceful하게 처리하는 경로가 없음. - tesla (
issue_service.rb:122-123): issue-service의 400 응답을Cupix::Logger.error로 기록하여, 정상적인 상황(IssueType 미존재)이 에러 로그로 집계됨.
AGT5210, AGT5208, TES11000, AGT5202, AGT5105 등의 error_code에 IssueType 매핑이 없는 것은 데이터 누락이 아닌 정상 상태이며, 코드가 이 상황을 에러로 처리하는 것이 근본 원인이다.
Technical Analysis#
Code Path#
1. Entry point: 캡처 처리 실패 시 이슈 생성 트리거
캡처가 처리 중 에러 상태로 전환될 때 Issueable concern을 통해 create_issue가 호출된다.
app/models/concerns/reconstruction.rb:65- reconstruction이:error상태로 전환 시 트리거app/models/concerns/statable/capture.rb:158- capture가:done상태로 전환 시 error code가 있으면 트리거
2. IssueService에서 REST 요청 구성
app/services/cupix/issue_service.rb:3-42:
# app/services/cupix/issue_service.rb:3-30
def create_issue(capture:, error_code:, issued_by: nil, async: true)
model_data = CaptureSerializer.new(capture, {
fields: {
capture: %i[id team facility record level editing captured_at created_at]
}
}).serializable_hash[:data][:attributes]
model_data[:type] = 'Capture'
# ...
body = {
model: model_data,
error_code: error_code,
issued_by: issued_by.presence || '',
created_by: capture.user.email,
contact_email: contact_email,
cc_addresses: cc_addresses(capture.team),
se_addresses: se_addresses(capture.team)
}
요청 body에 error_code(예: AGT5210)가 포함되며, issue_type_id는 전송하지 않는다. issue-service가 error_code로 IssueType을 조회하는 구조이다.
3. REST POST 호출 및 에러 로깅
app/services/cupix/issue_service.rb:107-131:
# app/services/cupix/issue_service.rb:107-127
def _post(body)
url = "#{$CUPIX_ISSUE_SERVICE_URL}/issues"
begin
resp = RestClient.post(
url,
body,
content_type: :json,
authorization: "Bearer #{_access_token}"
)
resp = Oj.load(resp)
rescue RestClient::Exception => e
Cupix::Logger.error("fail to post to issue service. reason: 'RestClient error' message: #{e.response}",
class: self.name, function: __method__)
rescue StandardError => e
Cupix::Logger.error("fail to post message to issue service. reason: 'Standard error' message: #{e.message}",
class: self.name, function: __method__)
end
resp
end
issue-service가 HTTP 400을 반환하면 RestClient::Exception이 발생하고, e.response에 담긴 JSON body(ARG10002)가 에러 로그에 포함된다. Failure point는 이 rescue 블록이다.
4. issue-service Lambda에서 IssueType 조회 실패 (수신 측)
applications/issue-service/src/lambda/issue/create.ts:76-84:
// applications/issue-service/src/lambda/issue/create.ts:76-84
let issueType: IssueType | null;
try {
issueType = await findIssueType(data.issue_type_id, data.error_code);
if (!issueType) {
return failure({ status: 400, code: 'ARG10002', message: "IssueType not found"});
}
} catch (e) {
return failure({ status: 500, code: 'SYS50000', message: "Internal server error: " + JSON.stringify(e) });
}
findIssueType은 data.issue_type_id가 없으면(tesla는 전송하지 않음) error_code로 DynamoDB GSI를 조회한다.
5. DynamoDB ErrorCodeIndex GSI 조회
applications/issue-service/src/common/dynamodb-helper.ts:45-66:
// applications/issue-service/src/common/dynamodb-helper.ts:45-66
export const getIssueTypByErrorCode = async (client: DynamoDBClient, errorCode: string | undefined) => {
if (!errorCode) { return null; }
const issueTypeParams = {
TableName: issueTypeTableName,
IndexName: issueTypeErrorCodeIndexName,
KeyConditionExpression: 'error_code = :error_code',
ExpressionAttributeValues: marshall({
':error_code': errorCode,
}),
ScanIndexForward: false,
Limit: 1
};
const issueType = await client.send(new QueryCommand(issueTypeParams))
.then(result => result.Items && result.Items[0])
.then(item => item && unmarshall(item));
return issueType as IssueType;
};
ErrorCodeIndex GSI에서 해당 error_code로 조회한 결과가 없으면 null이 반환되고, create.ts에서 ARG10002를 응답한다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixvista-api-worker status:error "fail to post to issue service"
service:cupixvista-api-worker "ARG10002"
service:cupixvista-api-worker @request_id:<jid>
에러 로그 (production, 8건 중 대표):
{
"message": "fail to post to issue service. reason: 'RestClient error' message: {\"result\":{\"code\":\"ARG10002\",\"message\":\"IssueType not found\"}}",
"class": "Cupix::IssueService",
"function": "_post",
"role": "worker",
"tenant": "cupix",
"host": "ip-10-1-82-141"
}
Sidekiq job 로그 (request_id로 조회한 context):
{
"message": "done",
"worker": "IssueServiceWorker",
"queue": "default",
"args": [14333, "{\"error_code\":\"AGT5210\",\"model\":{\"type\":\"Capture\",\"id\":14333,\"team\":{\"id\":3876}}}"],
"duration": 1.972,
"job_status": "done"
}
타임라인 (production):
| 시간 (UTC) | Error Code | Team | Capture ID |
|---|---|---|---|
| 10:35:10 | AGT5210 |
Gmail's Team (3876) | 14333 |
| 12:25:37 | AGT5208 |
Pracht (3216) | 14336 |
| 18:43:39 | TES11000 |
Jorge (2630) | 5427 |
| 19:55:50 | AGT5210 |
Byron (5221) | 14358 |
| 19:56:10 | AGT5202 |
Byron (5221) | 14360 |
| 23:39:02 | AGT5105 |
ParallelDS (3940) | 14362 |
| 23:45:14 | AGT5210 |
ParallelDS (3940) | 14363 |
핵심 관찰:
- 5개의 서로 다른
error_code(AGT5210,AGT5208,TES11000,AGT5202,AGT5105)에서 모두 동일하게 실패 - 5개의 서로 다른 팀/사용자에서 발생 -- 특정 사용자 문제가 아님
- 2개의 production deploy(
689a4554,7ff48a81)에 걸쳐 발생 -- 코드 배포 문제가 아님 - dev 환경에서도 동일하게 발생 (02:52:58,
ip-10-192-19-67) - Worker는 에러를 catch하고 "done"으로 완료 처리 -- 재시도 없음
Fix Recommendation#
Option A: issue-service validation 수정 (권장)#
파일: applications/issue-service/src/lambda/issue/create.ts:76-84
IssueType이 없어도 이슈 생성을 허용하도록 validation 로직을 수정한다. IssueType이 없는 경우 issue_type_id 없이 이슈를 생성하거나, 200/204로 graceful하게 응답하여 caller가 에러로 처리하지 않도록 한다.
// 현재: IssueType 없으면 400 에러
if (!issueType) {
return failure({ status: 400, code: 'ARG10002', message: "IssueType not found"});
}
// 수정안: IssueType 없어도 이슈 생성 허용 (issueType 관련 필드만 null)
buildIssueTypeCreateParams (create.ts:49-63)는 이미 issueTypeId가 없으면 undefined를 반환하고, params.RequestItems 배열에서 filter(isNotNull)로 제거되므로 (create.ts:94), validation 블록만 수정하면 나머지 로직은 정상 동작한다.
Option B: tesla 로그 레벨/내용 변경#
파일: app/services/cupix/issue_service.rb:122-123
RestClient::Exception catch에서 응답 body를 파싱하여 ARG10002("IssueType not found")인 경우 error 대신 warn 또는 info로 로그 레벨을 낮춘다.
# 현재: 모든 RestClient 에러를 동일하게 error 레벨로 기록
rescue RestClient::Exception => e
Cupix::Logger.error("fail to post to issue service. reason: 'RestClient error' message: #{e.response}",
class: self.name, function: __method__)
# 수정안: ARG10002는 warn으로 분리
rescue RestClient::Exception => e
resp_body = Oj.safe_load(e.response.to_s) rescue nil
if resp_body&.dig('result', 'code') == 'ARG10002'
Cupix::Logger.warn("issue type not found for error_code, skipping issue creation",
class: self.name, function: __method__)
else
Cupix::Logger.error("fail to post to issue service. reason: 'RestClient error' message: #{e.response}",
class: self.name, function: __method__)
end
단기 개선#
- 두 Option을 함께 적용하면 가장 효과적: issue-service가 IssueType 없이도 동작하고, tesla가 해당 응답을 에러로 기록하지 않음
- issue-service의
create.ts에서IssueType을 찾지 못했을 때 어떤error_code로 조회를 시도했는지 응답에 포함하여 디버깅 용이성 개선
Monitoring#
- 에러 발생 알림:
service:cupixvista-api-worker status:error "ARG10002"
위 쿼리로 Datadog monitor를 설정하여 IssueType not found 에러 재발 시 즉시 알림
- IssueServiceWorker 성공률 추적:
service:cupixvista-api-worker "IssueServiceWorker" "done" -"fail to post"
정상 완료된 job과 실패한 job의 비율을 대시보드로 모니터링
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- IssueType 매핑이 없는 error_code에 대해 이슈가 생성되지 않는 것은 정상 동작이며, 실제 기능 장애가 아닌 로그 노이즈 문제. 캡처 처리 자체에는 영향 없음 (worker가 에러를 catch 후 정상 완료). 에러 로그가 불필요하게 모니터링 알림을 유발할 수 있어 로그 레벨 조정이 필요.
Revision History#
Revision 1#
Feedback: issue type 이 없는건 정상임. issue_type validation 을 수정하던가 로그 레벨, 내용을 변경하는식으로 수정
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| IssueType이 없는 것은 정상 동작이며, DynamoDB 데이터 누락이 아님 | 수용 | applications/issue-service/schema/seed.json:145-194: IssueType 테이블에 AGT10004, ARG10000 2개만 시드 데이터 존재. 에러 로그의 AGT5210, AGT5208, TES11000, AGT5202, AGT5105는 처음부터 매핑이 설계되지 않음. app/services/cupix/issue_service.rb:44-56의 skip_create_issue?도 일부 error_code만 필터링하며, 모든 error_code에 IssueType이 존재하도록 설계된 것이 아님이 확인됨. |
| Fix 방향을 issue_type validation 수정 또는 로그 레벨/내용 변경으로 전환 | 수용 | create.ts:79-81: IssueType null일 때 무조건 400 반환하는 validation이 과도함. create.ts:49-63의 buildIssueTypeCreateParams는 이미 issueTypeId 없으면 undefined 반환하고 filter(isNotNull) (line 94)로 제거되므로, validation만 수정하면 IssueType 없이도 이슈 생성 가능. issue_service.rb:122-123: RestClient::Exception을 무조건 error 레벨로 기록하여 정상 상황이 에러로 집계됨 — warn/info로 변경하여 로그 노이즈 제거 가능. |
변경 사항:
## Root Cause Summary: "DynamoDB IssueType 테이블 데이터 누락" → "IssueType 미존재가 정상인 상황을 코드가 에러로 처리하는 것이 근본 원인"으로 수정## Fix Recommendation: DynamoDB 데이터 복구 → Option A (issue-service validation 수정) / Option B (tesla 로그 레벨 변경) 두 가지 방향으로 전면 재작성## Risk Assessment: risk level을 medium → low로 하향 (실제 기능 장애가 아닌 로그 노이즈 문제)
추가 조사 내용:
applications/issue-service/schema/seed.json— IssueType 시드 데이터 확인 (AGT10004, ARG10000만 매핑 존재)applications/issue-service/src/lambda/issue/create.ts:49-63, 86-96—buildIssueTypeCreateParams가 issueTypeId 없이도 동작 가능함을 확인app/services/cupix/issue_service.rb:44-56—skip_create_issue?필터 범위 확인