Skip to content

Alert "Time" shows the UTC value labeled with the local timezone when the host timezone is not UTC #2304

Description

@hiromi-dev

Describe the bug

AlertModel.__init__ converts detected_at with detected_at.astimezone(...). The detected_at values loaded from the alerts table are naive datetimes stored in UTC, and astimezone() interprets a naive datetime as local time. As a result, when edr monitor runs on a machine whose timezone is not UTC, the "Time:" field of every alert (and formatted_detected_at in the source freshness result description) shows the UTC value labeled with the local timezone name.

In the same source freshness message, max_loaded_at goes through convert_datetime_utc_str_to_timezone_str(), which correctly treats the value as UTC. So a single message mixes two different timezones.

Real-world example (container with TZ=Asia/Tokyo, no timezone config; the check actually ran at 18:08:22 JST = 09:08:22 UTC):

Time: 2026-07-22 09:08:22 JST
When the test ran at 2026-07-22 09:08:22 JST, the most recent record found in
the table was 1 day 0h 11m 41s earlier (2026-07-21 17:55:10 JST).

Note the internal inconsistency: 09:08:22 − (17:55:10 the previous day) is 15h13m, not "1 day 0h 11m 41s". The elapsed time (computed in UTC) and max_loaded_at (correctly converted to JST) are right; only the "test ran at" timestamp is mislabeled. This misled our team into believing the alert had been delayed by 9 hours.

To Reproduce

Run the following on a machine whose timezone is not UTC (or keep the two TZ lines to force it):

import os, time
os.environ["TZ"] = "Asia/Tokyo"; time.tzset()

from datetime import datetime
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel

alert = SourceFreshnessAlertModel(
    id="x", source_name="s", identifier="t", original_status="fail",
    path="p", error=None, alert_class_id="c", source_freshness_execution_id="e",
    detected_at=datetime(2026, 7, 22, 9, 8, 22),          # naive UTC, as loaded from the alerts table
    max_loaded_at=datetime(2026, 7, 21, 8, 55, 10),       # naive UTC
    max_loaded_at_time_ago_in_s=87101.8,
)
print(alert.detected_at_str)      # 2026-07-22 09:08:22 JST  <- UTC value, JST label
print(alert.result_description)   # mixes 09:08:22 "JST" with a correctly converted 17:55:10 JST

Setting the timezone config does not help while the host timezone is non-UTC: the naive value is assumed to be local, so converting local→local is a no-op.

Host TZ timezone config "Time:" shown correct?
Asia/Tokyo (none) 09:08:22 JST
UTC (none) 09:08:22 UTC
UTC Asia/Tokyo 18:08:22 JST
Asia/Tokyo Asia/Tokyo 09:08:22 JST

Expected behavior

Naive detected_at should be interpreted as UTC (matching how it is stored and how max_loaded_at is handled in the same message), so the example above renders Time: 2026-07-22 18:08:22 JST.

Screenshots

Environment (please complete the following information):

  • Elementary CLI (edr) version: 0.23.4 (the relevant code is unchanged on v0.25.1 and current master)
  • Elementary dbt package version: 0.25.1
  • dbt version you're using: 1.11.12
  • Data warehouse: snowflake
  • Infrastructure details: Linux Docker container with ENV TZ Asia/Tokyo, running edr monitor on AWS ECS Fargate (prod)

Additional context

Affects the "Time:" field of all alert types via AlertModel.detected_at_str, and additionally formatted_detected_at inside the source freshness result description. Deployments whose host timezone is UTC never see the bug, which is probably why it hasn't been reported.

Would you be willing to contribute a fix for this issue?

Yes — I have a fix ready (a few lines in elementary/monitor/alerts/alert.py interpreting naive detected_at as UTC before astimezone, plus unit tests) and will open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions