Failed to lookup timezone from location
RCA: Failed to lookup timezone from location
Overview#
What Happened#
cupixworks-api의 Facility#set_location_based_timezone! 메서드에서 Timezone.lookup(latitude, longitude) 호출이 Timezone::Error::InvalidZone 예외로 실패했다. 동일 시간대(2026-06-30 01:42 ~ 01:54 KST, 약 12분 윈도우)에 인접한 facility.id (21380, 21381) 두 건에서 총 3회 발생했으며, 호출 경로 상의 상위 rescue에서 fallback 처리되어 사용자에게 5xx 응답으로 전파되지는 않았다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Timezone::Error::InvalidZone |
| exception.message | Timezone::Error::InvalidZone |
| top_frame | app/models/concerns/has_timezone.rb:10 |
| runtime | Ruby on Rails (Rails monolith, tesla repo); timezone gem 1.3.16 with :google lookup backend |
| deploy | production-us-west-2-20260629T0754Z0-bfdc5ebd-cupixworks |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupix (tenant) — Facility 관리 | 3 | 신규/업데이트된 Facility 두 건에 대해 위치 기반 timezone 자동 설정이 실패. fallback 경로(set_default_timezone 또는 set_location_based_timezone)에서 team timezone 또는 UTC offset으로 대체되었거나 timezone 갱신이 silently no-op 처리됨. 사용자 facing 5xx 없음. |
Timeline#
- 2026-06-30 01:42 KST — Facility(
id=21380, key=qupl2i)에서 첫Timezone::Error::InvalidZone발생 (request_id/si_trace_id동일,si_trace_origin: api_request). 클러스터 first_seen. - 2026-06-30 01:49 KST — 같은 Facility(
id=21380, key=qupl2i)에서 동일 에러 재발생. - 2026-06-30 01:54 KST — 인접 Facility(
id=21381, key=xgvou2)에서 동일 에러 발생. 이후 14일 retention 내 추가 발생 없음.
Error Log#
Failed to lookup timezone from location
Impact#
- Service:
cupixworks-api - Team: cupix
- 발생 횟수: 3 (Datadog 14일 retention 기준; 클러스터 frontmatter는 first_seen 시점의 1건만 기록)
- 최초 발생: 2026-06-30 01:42 KST
- 최근 발생: 2026-06-30 01:54 KST (Datadog)
Root Cause Summary#
Facility#set_location_based_timezone!는 모델에 저장된 latitude, longitude로 Timezone.lookup을 호출한다. 이 호출은 config/initializers/timezone.rb의 설정에 따라 Google Maps Timezone API(:google backend)로 원격 lookup을 수행한다. Google 응답에서 유효한 timezone을 식별하지 못했을 때 (예: ZERO_RESULTS, 미정의 zone, 좌표가 해상/극지/0,0 등 timezone 매핑이 없는 위치) timezone gem이 Timezone::Error::InvalidZone을 raise한다. 이는 잘못된/경계 좌표 값에 대해 발생하는 데이터 품질 이슈이며, cupixworks-api 코드 결함이 아니다. 또한 상위 caller(set_default_timezone, set_location_based_timezone)가 rescue StandardError로 받아 fallback 처리하므로 요청은 정상 완료되지만, has_timezone.rb의 inner rescue가 동일 사건을 error level로 한 번 더 로깅하면서 Datadog 에러 신호로 잡힌 것이 이번 클러스터의 본질이다.
Technical Analysis#
Code Path#
Entry point: 두 가지 진입로가 존재한다.
before_create :set_default_timezone(Facility 생성 시)before_commit :update_timezone, on: :update(Facility의location컬럼 변경 시)
핵심 lookup 지점과 rescue 구조:
def set_location_based_timezone!
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'Location not set') if location.blank?
timezone = Timezone.lookup(latitude, longitude)
self.timezone_name = timezone.name
self.timezone_offset = timezone.utc_offset
Cupix::Logger.info("Set location based timezone on facility #{self.key}. timezone_offset: #{timezone.utc_offset} timezone_name: #{timezone.name}", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key })
rescue Cupix::Errors::Parameter => e
# Location not set - expected fallback case, no error log needed
raise e
rescue StandardError => e
# Actual timezone lookup failure
Cupix::Logger.error('Failed to lookup timezone from location',
class: self.class.name,
function: __method__,
facility: { id: self.id, key: self.key },
error: e.message,
error_type: e.class.name)
raise e
else
timezone
end
상위 rescue:
def set_default_timezone
set_location_based_timezone!
rescue StandardError => e
if self.team.try(:timezone_offset).present?
self.timezone_offset = self.team.timezone_offset
else
self.timezone_offset = Time.zone.utc_offset
Cupix::Logger.warn("Timezone offset not set for model. model: #{self.class.name}, reason: #{e.message}", class: self.class.name, function: __method__)
end
end
def update_timezone
return unless location_changed?
Cupix::Logger.info("update timezone on facility #{self.key}.", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key, saved_changes: saved_changes })
self.set_location_based_timezone
end
set_location_based_timezone (non-bang) 역시 rescue StandardError → false로 silently 흡수한다 (has_timezone.rb:31-38).
Lookup backend 설정:
GOOGLE_API_KEY = ENV['GOOGLE_API_KEY'] || 'AIzaSyACNIOJof_WCGjD8Ol4gNkyHnd27_7EKMI'
Timezone::Lookup.config(:google) do |c|
c.api_key = GOOGLE_API_KEY
end
Gem 계약 (sorbet RBI):
# @raise [Timezone::Error::Lookup] if the remote lookup fails
# @raise [Timezone::Error::InvalidZone] if the remote lookup
# succeeds but the resulting timezone is not found and a default
# value or block has not been provided
class Timezone::Error::InvalidZone < ::Timezone::Error::Base; end
기대 동작: 유효한 (lat, lng)에 대해 Timezone::Zone 객체를 반환 → timezone_name, timezone_offset 세팅.
실제 동작: Google API가 응답은 성공시켰지만 해당 좌표에 매핑되는 zone을 식별하지 못함 → InvalidZone raise → inner rescue가 error 로그 출력 후 re-raise → 상위 set_default_timezone/set_location_based_timezone가 fallback 처리.
Failure point: app/models/concerns/has_timezone.rb:10 (Timezone.lookup(latitude, longitude)).
Log Evidence#
Datadog query (재현용):
service:cupixworks-api "Failed to lookup timezone from location"
기간: now-2d (RCA 시점 기준). 14일 retention 전체에서도 동일 3건만 반환됨.
대표 로그 (raw 페이로드 핵심 필드):
{
"timestamp": "2026-06-29T16:54:48.218Z",
"status": "error",
"message": "Failed to lookup timezone from location",
"class": "Facility",
"function": "set_location_based_timezone!",
"error": { "msg": "Timezone::Error::InvalidZone" },
"error_type": "Timezone::Error::InvalidZone",
"facility": { "id": 21381, "key": "xgvou2" },
"request_id": "ce2fface-08dc-4c41-ae7f-b773ac4a4d1a",
"si_trace_origin": "api_request",
"environment": "production",
"service": "cupixworks-api",
"dd": {
"version": "production-us-west-2-20260629T0754Z0-bfdc5ebd-cupixworks"
}
}
{
"timestamp": "2026-06-29T16:49:05.899Z",
"facility": { "id": 21380, "key": "qupl2i" },
"request_id": "e92c2d74-4266-4471-9989-e8c3425fa7bd",
"error_type": "Timezone::Error::InvalidZone"
}
{
"timestamp": "2026-06-29T16:42:59.256Z",
"facility": { "id": 21380, "key": "qupl2i" },
"request_id": "dcba9c26-435a-468b-be2d-ee5ce075f7bd",
"error_type": "Timezone::Error::InvalidZone"
}
비교 신호 — 같은 시간대의 Timezone offset not set warn 로그는 Capture 클래스 + reason: Location not set이며 본 클러스터의 Facility 케이스와 다른 경로(Cupix::Errors::Parameter)다. 즉 본 사건은 "좌표는 있으나 Google이 zone 매핑 실패"한 케이스로 한정된다.
호스트/배포 정보가 모두 동일(ip-10-1-144-228.us-west-2.compute.internal, 동일 빌드 bfdc5ebd)하고 다른 호스트에서는 발생하지 않은 점, 단일 tenant(cupix)에서만 발생한 점으로 미루어 광역 인프라 이슈가 아닌 좌표 데이터 측 요인이다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Facility의 (latitude, longitude)가 timezone 매핑이 없는 영역(해상/극지/(0,0) 등 경계 좌표)이라 Google Timezone API가 zone을 식별하지 못해 InvalidZone이 raise됨 |
(a) error_type: Timezone::Error::InvalidZone 명시; (b) gem RBI(timezone@1.3.16.rbi:46-49)가 remote lookup 성공 후 zone 미발견 시 정확히 이 예외를 raise한다고 명시; (c) set_location_based_timezone!는 location.blank?를 사전 가드하므로 nil 좌표는 별도 Cupix::Errors::Parameter 경로로 빠짐 — 즉 좌표는 존재함; (d) Google backend 구성(config/initializers/timezone.rb:3) |
— | Confirmed |
| H2 | Google Maps Timezone API 자체 장애(429/5xx, 네트워크 timeout) | 동일 시간대에 Google 측 광역 장애 시 다른 서비스/호스트에서도 발생할 것이나, 같은 호스트·동일 tenant·인접 facility id에 집중. Timezone::Error::Lookup(원격 실패용)이 아닌 InvalidZone(zone 미발견용)이 raise됨 — gem이 두 케이스를 분리해서 던진다 |
— | Rejected |
| H3 | Application 코드 결함(잘못된 좌표 전달, nil 처리 누락) | — | has_timezone.rb:8에서 location.blank? 가드 후 lookup; 좌표 자체는 모델 컬럼에서 직접 읽음. Bang 메서드의 모든 호출처(set_default_timezone, set_location_based_timezone, cupix/migrate/facility.rb:125)가 rescue로 감싸 fallback을 제공. 실제 응답에 영향 없음 |
Rejected |
| H4 | 환경 설정 누락(GOOGLE_API_KEY 미설정, 잘못된 키) |
— | 초기화 파일에 fallback 키가 인라인되어 항상 설정값이 존재. 광역 인증 실패라면 모든 호출이 실패하고 빈도가 훨씬 높을 것 (14일 동안 3건만 발생) | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
없음. 사용자 영향이 확인되지 않은 데이터 품질 사건이다. set_default_timezone(has_timezone.rb:40-49)과 set_location_based_timezone(has_timezone.rb:31-38)이 이미 fallback을 제공하고 있어 5xx 전파는 없다. cupix/migrate/facility.rb:125의 직접 호출 경로도 외부 rescue StandardError로 감싸져 있다(facility.rb:127-128).
단기 개선 (1주 이내)#
app/models/concerns/has_timezone.rb:20-25의 로그 레벨을error→warn으로 다운그레이드 검토. 근거:- 본 예외는 외부 API(Google Timezone)가 정상 응답한 결과로 "이 좌표에는 zone이 없다"는 도메인 결과일 수 있어 항상 actionable한 에러가 아님.
- 상위 caller가 fallback 처리하므로 사용자/요청 측 영향이 없음. 메모리(MEMORY.md "Assess error severity during RCA" 패턴)에 일치하는 케이스.
- 단,
Timezone::Error::Lookup(원격 호출 자체 실패)은 별도로 분기하여 여전히error로 남기는 것을 권장. gem RBI(timezone@1.3.16.rbi:46-49)가 두 예외를 분리한다.
- 로그 페이로드에
latitude,longitude값을 추가하여 재발 시 좌표 자체를 즉시 진단할 수 있도록 한다. PII가 아닌 시설 위치이므로 로깅해도 무방. (has_timezone.rb:20-25로그 인자에location: { lat:, lng: }추가)
장기 개선 (재발 방지)#
- Facility 생성/수정 입력 검증 단계에서 timezone lookup 가능성을 사전 체크하는 옵션 고려: 클라이언트가 위치 입력 시 Google Geocoding/Timezone을 통해 사전 검증한 좌표를 사용하거나, 명백히 무효한 좌표((0,0), |lat| > 85 등 극지)를 입력 단에서 거절.
- Lookup backend 다중화: Google API의 zone 미발견에 대비해
tzinfo로컬 zoneinfo + 좌표→zone 매핑(timezone_finder/tz_lookup등) fallback을 도입하면 외부 의존성 또한 줄일 수 있음. 검토 후 결정. config/initializers/timezone.rb:1의 하드코딩된 fallback API key는 본 RCA 범위 밖이지만 별도 이슈로 분리 권장 (uncertain — needs verification: 해당 키가 현재 유효한지/누출 위험이 있는지 보안 팀 확인 필요).
Monitoring#
권장 메트릭/알림. 모두 timeseries widget에 그대로 사용할 수 있는 query 형태:
- 본 에러 발생 추이 (현 클러스터 재현)
logs("service:cupixworks-api status:error \"Failed to lookup timezone from location\"").index("*").rollup("count").by("error_type").rollup("count", "5m")
- Facility 모델 timezone lookup 실패 분포 (Lookup vs InvalidZone 분리 확인용)
logs("service:cupixworks-api @class:Facility @function:set_location_based_timezone! status:error").index("*").rollup("count").by("@error_type").rollup("count", "5m")
- Fallback 트리거 빈도 (warn 경로) — 본 사건과 별개의
Location not set경로 추이도 같이 본다
logs("service:cupixworks-api \"Timezone offset not set\" status:warn").index("*").rollup("count").by("@class").rollup("count", "5m")
알림 임계 권장: 단일 cluster 기준 5분 윈도우에서 Timezone::Error::InvalidZone 5건 초과 시 알림. 본 사건 빈도(3건/12분)는 의미 있는 burst이긴 하나 페이지 수준은 아님.
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (단기 개선만 적용할 경우 — 로그 레벨/페이로드 조정 수준)