ES /docs

Failed to lookup timezone from location

RCA: Failed to lookup timezone from location

Overview#

What Happened#

cupixworks-apiFacility#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#

  1. 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.
  2. 2026-06-30 01:49 KST — 같은 Facility(id=21380, key=qupl2i)에서 동일 에러 재발생.
  3. 2026-06-30 01:54 KST — 인접 Facility(id=21381, key=xgvou2)에서 동일 에러 발생. 이후 14일 retention 내 추가 발생 없음.

Error Log#

Datadog Logs

text
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, longitudeTimezone.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 구조:

app/models/concerns/has_timezone.rb:7-29ruby
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:

app/models/concerns/has_timezone.rb:40-49ruby
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
app/models/concerns/geo_queriable/facility.rb:12-17ruby
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 설정:

config/initializers/timezone.rb:1-5ruby
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):

sorbet/rbi/gems/timezone@1.3.16.rbi:46-49,106ruby
# @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 (재현용):

text
service:cupixworks-api "Failed to lookup timezone from location"

기간: now-2d (RCA 시점 기준). 14일 retention 전체에서도 동일 3건만 반환됨.

대표 로그 (raw 페이로드 핵심 필드):

json
{
  "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"
  }
}
json
{
  "timestamp": "2026-06-29T16:49:05.899Z",
  "facility": { "id": 21380, "key": "qupl2i" },
  "request_id": "e92c2d74-4266-4471-9989-e8c3425fa7bd",
  "error_type": "Timezone::Error::InvalidZone"
}
json
{
  "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의 로그 레벨을 errorwarn으로 다운그레이드 검토. 근거:
    • 본 예외는 외부 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 형태:

  • 본 에러 발생 추이 (현 클러스터 재현)
text
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 분리 확인용)
text
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 경로 추이도 같이 본다
text
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 (단기 개선만 적용할 경우 — 로그 레벨/페이로드 조정 수준)