ES /docs

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#

Datadog Logs

text
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-workerIssueServiceWorker가 캡처 처리 실패 시 issue-service(cupixworks 모노레포의 Lambda)에 이슈 생성 요청을 보내는데, IssueType 매핑이 없는 error_code에 대해서도 요청이 전송되고 있다. IssueType이 모든 error_code에 존재하는 것이 아니라, 일부 error_code에만 매핑이 존재하는 것이 정상 설계이다 (seed.json 기준 AGT10004, ARG10000만 IssueType 매핑 보유). 문제는 두 곳에 있다:

  1. issue-service (create.ts:79-81): IssueType을 찾지 못하면 무조건 HTTP 400 ARG10002를 반환하는 validation이 과도함. IssueType 없이도 이슈 생성을 허용하거나 graceful하게 처리하는 경로가 없음.
  2. 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:

ruby
# 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_codeIssueType을 조회하는 구조이다.

3. REST POST 호출 및 에러 로깅

app/services/cupix/issue_service.rb:107-131:

ruby
# 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:

typescript
// 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) });
}

findIssueTypedata.issue_type_id가 없으면(tesla는 전송하지 않음) error_code로 DynamoDB GSI를 조회한다.

5. DynamoDB ErrorCodeIndex GSI 조회

applications/issue-service/src/common/dynamodb-helper.ts:45-66:

typescript
// 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 쿼리:

text
service:cupixvista-api-worker status:error "fail to post to issue service"
text
service:cupixvista-api-worker "ARG10002"
text
service:cupixvista-api-worker @request_id:<jid>

에러 로그 (production, 8건 중 대표):

json
{
  "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):

json
{
  "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가 에러로 처리하지 않도록 한다.

typescript
// 현재: 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로 로그 레벨을 낮춘다.

ruby
# 현재: 모든 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#

  • 에러 발생 알림:
text
service:cupixvista-api-worker status:error "ARG10002"

위 쿼리로 Datadog monitor를 설정하여 IssueType not found 에러 재발 시 즉시 알림

  • IssueServiceWorker 성공률 추적:
text
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-56skip_create_issue?도 일부 error_code만 필터링하며, 모든 error_code에 IssueType이 존재하도록 설계된 것이 아님이 확인됨.
Fix 방향을 issue_type validation 수정 또는 로그 레벨/내용 변경으로 전환 수용 create.ts:79-81: IssueType null일 때 무조건 400 반환하는 validation이 과도함. create.ts:49-63buildIssueTypeCreateParams는 이미 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-96buildIssueTypeCreateParams가 issueTypeId 없이도 동작 가능함을 확인
  • app/services/cupix/issue_service.rb:44-56skip_create_issue? 필터 범위 확인