Skip to content

Commit 27573d8

Browse files
committed
19994 FIX HTML email: Use originating site's timezone for 'Event date'
SUP-29059 Change-Id: Ia8fa86784ee9dbcc0fb9cb14701d26c598d362b1
1 parent 2f460bd commit 27573d8

6 files changed

Lines changed: 102 additions & 5 deletions

File tree

.werks/19994.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[//]: # (werk v3)
2+
# HTML email: Use originating site's timezone for 'Event date'
3+
4+
key | value
5+
---------- | ---
6+
date | 2026-05-19T06:00:02.416865+00:00
7+
version | 2.5.0p4
8+
class | fix
9+
edition | community
10+
component | notifications
11+
level | 1
12+
compatible | yes
13+
14+
In distributed setups, HTML email notifications previously rendered the
15+
"Event date" using the timezone of the site that sent the email. When the
16+
central site processed notifications forwarded from a remote site in a
17+
different timezone, the timestamp neither reflected when the event actually
18+
occurred on the remote nor included a timezone indicator, making it
19+
ambiguous.
20+
21+
The state-change timestamps for "Event date" are now pre-rendered on the
22+
originating site, so the email shows the event time in the timezone of the
23+
site where the state change happened and includes the timezone abbreviation,
24+
e.g. "2026-05-19 14:32:11 AEST".
25+
26+
This also affects the "Event date" column in HTML bulk notifications.

cmk/base/events.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,19 @@ def complete_raw_context(
356356
# from that one, but we try to keep this simple here.
357357
enriched_context["MICROTIME"] = "%d" % (time.time() * 1000000)
358358

359+
# Pre-render the state-change timestamps in the originating site's local
360+
# timezone. In distributed setups the central site processes forwarded
361+
# notifications in its own timezone, which would otherwise hide where
362+
# and when the event actually happened.
363+
if host_state_change := enriched_context.get("LASTHOSTSTATECHANGE"):
364+
enriched_context["LASTHOSTSTATECHANGE_LOCAL"] = time.strftime(
365+
"%Y-%m-%d %H:%M:%S %Z", time.localtime(int(host_state_change))
366+
)
367+
if service_state_change := enriched_context.get("LASTSERVICESTATECHANGE"):
368+
enriched_context["LASTSERVICESTATECHANGE_LOCAL"] = time.strftime(
369+
"%Y-%m-%d %H:%M:%S %Z", time.localtime(int(service_state_change))
370+
)
371+
359372
enriched_context["HOSTURL"] = "/check_mk/index.py?start_url=view.py?%s" % quote(
360373
urlencode(
361374
[

notifications/templates/mail/bulk.html.jinja

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@
5656
</td>
5757
<td align="right" width="100%" style="padding: 0; vertical-align: middle">
5858
{% if bulk_service_notification %}
59-
{{ entry.LASTSERVICESTATECHANGE | timestamp() if entry.get('LASTSERVICESTATECHANGE') else entry.SHORTDATETIME }}
59+
{{ entry.LASTSERVICESTATECHANGE_LOCAL if entry.get("LASTSERVICESTATECHANGE_LOCAL") else (entry.LASTSERVICESTATECHANGE | timestamp() if entry.get('LASTSERVICESTATECHANGE') else entry.SHORTDATETIME) }}
6060
{% else %}
61-
{{ entry.LASTHOSTSTATECHANGE | timestamp() if entry.get('LASTHOSTSTATECHANGE') else entry.SHORTDATETIME }}
61+
{{ entry.LASTHOSTSTATECHANGE_LOCAL if entry.get("LASTHOSTSTATECHANGE_LOCAL") else (entry.LASTHOSTSTATECHANGE | timestamp() if entry.get('LASTHOSTSTATECHANGE') else entry.SHORTDATETIME) }}
6262
{% endif %}
6363
</td>
6464
</tr>

notifications/templates/mail/event_overview.html.jinja

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
<td width="120" style="padding:5px 0;font-weight:600;line-height:1.4;">Event date:</td>
3535
<td style="padding:5px 0;line-height:1.4;">
3636
{% if service_notification %}
37-
{{ data.get("LASTSERVICESTATECHANGE") | timestamp() if data.get('LASTSERVICESTATECHANGE') else 'N/A' }}
37+
{{ data.LASTSERVICESTATECHANGE_LOCAL if data.get("LASTSERVICESTATECHANGE_LOCAL") else (data.LASTSERVICESTATECHANGE | timestamp() if data.get('LASTSERVICESTATECHANGE') else 'N/A') }}
3838
{% else %}
39-
{{ data.get("LASTHOSTSTATECHANGE") | timestamp() if data.get('LASTHOSTSTATECHANGE') else 'N/A' }}
39+
{{ data.LASTHOSTSTATECHANGE_LOCAL if data.get("LASTHOSTSTATECHANGE_LOCAL") else (data.LASTHOSTSTATECHANGE | timestamp() if data.get('LASTHOSTSTATECHANGE') else 'N/A') }}
4040
{% endif %}
4141
</td>
4242
</tr>

packages/cmk-events/cmk/events/event_context.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ class EnrichedEventContext(EventContext, total=False):
8686
HOSTURL: str
8787
HOSTSHORTSTATE: str
8888
LASTHOSTSHORTSTATE: str
89+
LASTHOSTSTATECHANGE_LOCAL: str
8990
LASTHOSTSTATECHANGE_REL: str
9091
LASTHOSTUP_REL: str
9192
LASTSERVICESHORTSTATE: str
93+
LASTSERVICESTATECHANGE_LOCAL: str
9294
LASTSERVICESTATECHANGE_REL: str
9395
LASTSERVICEOK_REL: str
9496
PREVIOUSHOSTHARDSHORTSTATE: str

tests/unit/cmk/base/test_events.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@
33
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
44
# conditions defined in the file COPYING, which is part of this source code package.
55

6+
import datetime
67
from collections.abc import Mapping
78
from typing import Final
9+
from zoneinfo import ZoneInfo
810

911
import pytest
12+
import time_machine
1013
from pytest import MonkeyPatch
1114

1215
import cmk.base.events
1316
from cmk.base.events import (
1417
_update_enriched_context_from_notify_host_file,
1518
add_to_event_context,
1619
apply_matchers,
20+
complete_raw_context,
1721
event_match_hosttags,
1822
raw_context_from_string,
1923
)
20-
from cmk.events.event_context import EnrichedEventContext, EventContext
24+
from cmk.events.event_context import EnrichedEventContext, EventContext, HostName
2125
from cmk.utils.http_proxy_config import (
2226
EnvironmentProxyConfig,
2327
HTTPProxySpec,
@@ -60,6 +64,58 @@ def test_raw_context_from_string(context: str, expected: EventContext) -> None:
6064
assert raw_context_from_string(context) == expected
6165

6266

67+
@pytest.mark.parametrize(
68+
"tz, expected_host, expected_service",
69+
[
70+
("Australia/Brisbane", "2025-05-12 18:11:34 AEST", "2025-05-12 18:15:00 AEST"),
71+
("UTC", "2025-05-12 08:11:34 UTC", "2025-05-12 08:15:00 UTC"),
72+
],
73+
)
74+
def test_complete_raw_context_pre_formats_state_change_with_local_tz(
75+
tz: str, expected_host: str, expected_service: str
76+
) -> None:
77+
raw_context = EventContext(
78+
HOSTNAME=HostName("heute"),
79+
CONTACTS="cmkadmin",
80+
MICROTIME="1747037494000000",
81+
LASTHOSTSTATECHANGE="1747037494",
82+
LASTSERVICESTATECHANGE="1747037700",
83+
)
84+
85+
with time_machine.travel(datetime.datetime.now(tz=ZoneInfo(tz)), tick=False):
86+
enriched = complete_raw_context(
87+
raw_context,
88+
ensure_nagios=lambda _msg: None,
89+
with_dump=False,
90+
contacts_needed=False,
91+
analyse=False,
92+
)
93+
94+
assert enriched["LASTHOSTSTATECHANGE_LOCAL"] == expected_host
95+
assert enriched["LASTSERVICESTATECHANGE_LOCAL"] == expected_service
96+
97+
98+
def test_complete_raw_context_skips_local_field_for_host_notification() -> None:
99+
raw_context = EventContext(
100+
HOSTNAME=HostName("heute"),
101+
CONTACTS="cmkadmin",
102+
MICROTIME="1747037494000000",
103+
LASTHOSTSTATECHANGE="1747037494",
104+
)
105+
106+
with time_machine.travel(datetime.datetime.now(tz=ZoneInfo("UTC")), tick=False):
107+
enriched = complete_raw_context(
108+
raw_context,
109+
ensure_nagios=lambda _msg: None,
110+
with_dump=False,
111+
contacts_needed=False,
112+
analyse=False,
113+
)
114+
115+
assert enriched["LASTHOSTSTATECHANGE_LOCAL"] == "2025-05-12 08:11:34 UTC"
116+
assert "LASTSERVICESTATECHANGE_LOCAL" not in enriched
117+
118+
63119
def test_add_to_event_context_param_overrides_context() -> None:
64120
context = {"FOO": "bar", "BAZ": "old"}
65121
add_to_event_context(context, "BAZ", "new", lambda *args, **kw: HTTP_PROXY)

0 commit comments

Comments
 (0)