diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 05a5247..b08fa7b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -38,3 +38,4 @@ jobs:
         run: make network-test
       - name: Upload to codecov
         run: pipenv run codecov --token="${{ secrets.CODECOV_TOKEN }}"
+      - uses: codecov/codecov-action@v1
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 6345ace..980bf96 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -32,5 +32,4 @@ jobs:
         run: make integration-tests
       - name: Unit tests
         run: make unit-test
-      - name: Upload to codecov
-        run: pipenv run codecov
+      - uses: codecov/codecov-action@v1
diff --git a/docs/alerting.md b/docs/alerting.md
index 5f8005f..5448953 100644
--- a/docs/alerting.md
+++ b/docs/alerting.md
@@ -37,6 +37,7 @@ The section name should be the name of your alerter. This is the name you should
 | ooh_success | makes an alerter trigger its success action even if out-of-hours (0 or 1) | no | 0 |
 | groups | comma-separated list of group names this alerter will fire for. See the `group` setting for monitors | no | `default` |
 | only_failures | set to 1 to only fire this alerters for failure notifications (or catchups), not recoveries | no | 0 |
+| tz | timezone to use in alert messages | no | UTC |
 
 The *limit* uses the virtual fail count of a monitor, which means if a monitor has a tolerance of 3 and the alerter has a limit of 2, the monitor must fail 5 times before an alert is sent.
 
diff --git a/simplemonitor/Alerters/alerter.py b/simplemonitor/Alerters/alerter.py
index 34373af..4d0a3b7 100644
--- a/simplemonitor/Alerters/alerter.py
+++ b/simplemonitor/Alerters/alerter.py
@@ -158,6 +158,7 @@ class Alerter:
         self._only_failures = self.get_config_option(
             "only_failures", required_type=bool, default=False
         )
+        self._tz = cast(str, self.get_config_option("tz", default="UTC"))
 
         if self._ooh_failures is None:
             self._ooh_failures = []
@@ -311,9 +312,8 @@ class Alerter:
         else:
             return "unknowned"
 
-    @staticmethod
     def build_message(
-        length: AlertLength, alert_type: AlertType, monitor: Monitor
+        self, length: AlertLength, alert_type: AlertType, monitor: Monitor
     ) -> str:
         """Create a message for an Alerter to send."""
         if monitor.state() == MonitorState.FAILED:
@@ -336,7 +336,7 @@ class Alerter:
                 alert_type=alert_type.value,
                 alert_verb=Alerter._get_verb(alert_type),
                 downtime=downtime,
-                failure_time=format_datetime(monitor.first_failure_time()),
+                failure_time=format_datetime(monitor.first_failure_time(), self._tz),
                 monitor=monitor,
                 result=monitor.get_result(),
             )
@@ -384,13 +384,13 @@ class Alerter:
                 alert_type=alert_type,
                 monitor=monitor,
                 alert_verb=Alerter._get_verb(alert_type),
-                failure_time=format_datetime(monitor.first_failure_time()),
+                failure_time=format_datetime(monitor.first_failure_time(), self._tz),
                 downtime=downtime,
                 result=monitor.get_result(),
                 host=host,
                 desc=monitor.describe(),
                 vfc=monitor.virtual_fail_count(),
-                recovered_time=format_datetime(monitor.last_update),
+                recovered_time=format_datetime(monitor.last_update, self._tz),
             )
             message = textwrap.dedent(message)
         else:

Timezones in Alerters
Since #489 loggers now have a option `tz` for specifying a timezone (thanks for that!). Alerters don't have one though. So, for example, I can't change (or haven't found out how to) the timezone of the `{failed_at}` variable in the `execute` alerter. Maybe adding a `tz` option for alerters just like the one for loggers would be nice.
diff --git a/tests/test_alerter.py b/tests/test_alerter.py
index 3da5bda..f568b9d 100644
--- a/tests/test_alerter.py
+++ b/tests/test_alerter.py
@@ -297,12 +297,17 @@ class TestAlerter(unittest.TestCase):
 
 
 class TestMessageBuilding(unittest.TestCase):
+    def setUp(self):
+        self.test_alerter = alerter.Alerter()
+        self.freeze_time_value = "2020-03-10 09:00"
+        self.expected_time_string = "2020-03-10 09:00:00+00:00"
+
     def test_notification_format_failure(self):
         m = monitor.MonitorFail("test", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.NOTIFICATION, alerter.AlertType.FAILURE, m
                 ),
                 "Monitor test failed",
@@ -310,11 +315,11 @@ class TestMessageBuilding(unittest.TestCase):
 
     def test_notification_format_success(self):
         m = monitor.MonitorNull("winning", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             for _ in range(0, 6):
                 m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.NOTIFICATION, alerter.AlertType.SUCCESS, m
                 ),
                 "Monitor winning succeeded",
@@ -322,20 +327,21 @@ class TestMessageBuilding(unittest.TestCase):
 
     def test_oneline_format_failure(self):
         m = monitor.MonitorFail("test", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.ONELINE, alerter.AlertType.FAILURE, m
                 ),
-                "failure: test failed on {hostname} at 2020-03-10 09:00:00+00:00 (0+00:00:00): This monitor always fails.".format(
-                    hostname=util.short_hostname()
+                "failure: test failed on {hostname} at {expected_time} (0+00:00:00): This monitor always fails.".format(
+                    hostname=util.short_hostname(),
+                    expected_time=self.expected_time_string,
                 ),
             )
 
     def test_oneline_format_success(self):
         m = monitor.MonitorNull("winning", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             for _ in range(0, 6):
                 m.run_test()
             m.last_result = "a " * 80
@@ -345,32 +351,33 @@ class TestMessageBuilding(unittest.TestCase):
                 )
                 + "a " * 80
             )
-            output = alerter.Alerter.build_message(
+            output = self.test_alerter.build_message(
                 alerter.AlertLength.ONELINE, alerter.AlertType.SUCCESS, m
             )
             self.assertEqual(desired, output)
 
     def test_sms_format_failure(self):
         m = monitor.MonitorFail("test", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.SMS, alerter.AlertType.FAILURE, m
                 ),
-                "failure: test failed on {hostname} at 2020-03-10 09:00:00+00:00 (0+00:00:00): This monitor always fails.".format(
-                    hostname=util.short_hostname()
+                "failure: test failed on {hostname} at {expected_time} (0+00:00:00): This monitor always fails.".format(
+                    hostname=util.short_hostname(),
+                    expected_time=self.expected_time_string,
                 ),
             )
 
     def test_sms_format_success(self):
         m = monitor.MonitorNull("winning", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             for _ in range(0, 6):
                 m.run_test()
             m.last_result = "a " * 80
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.SMS, alerter.AlertType.SUCCESS, m
                 ),
                 "success: winning succeeded on {hostname} at (0+00:00:00): a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a...".format(
@@ -380,69 +387,79 @@ class TestMessageBuilding(unittest.TestCase):
 
     def test_full_format_failure(self):
         m = monitor.MonitorFail("test", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.FULL, alerter.AlertType.FAILURE, m
                 ),
                 textwrap.dedent(
                     """
                     Monitor test on {hostname} failed!
-                    Failed at: 2020-03-10 09:00:00+00:00 (down 0+00:00:00)
+                    Failed at: {expected_time} (down 0+00:00:00)
                     Virtual failure count: 1
                     Additional info: This monitor always fails.
                     Description: A monitor which always fails.
                     """.format(
-                        hostname=util.short_hostname()
+                        hostname=util.short_hostname(),
+                        expected_time=self.expected_time_string,
                     )
                 ),
             )
 
     def test_full_format_failure_docs(self):
         m = monitor.MonitorFail("test", {"failure_doc": "whoops"})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.FULL, alerter.AlertType.FAILURE, m
                 ),
                 textwrap.dedent(
                     """
                     Monitor test on {hostname} failed!
-                    Failed at: 2020-03-10 09:00:00+00:00 (down 0+00:00:00)
+                    Failed at: {expected_time} (down 0+00:00:00)
                     Virtual failure count: 1
                     Additional info: This monitor always fails.
                     Description: A monitor which always fails.
                     Documentation: whoops
                     """.format(
-                        hostname=util.short_hostname()
+                        hostname=util.short_hostname(),
+                        expected_time=self.expected_time_string,
                     )
                 ),
             )
 
     def test_full_format_success(self):
         m = monitor.MonitorNull("winning", {})
-        with freeze_time("2020-03-10 09:00"):
+        with freeze_time(self.freeze_time_value):
             for _ in range(0, 6):
                 m.run_test()
             self.assertEqual(
-                alerter.Alerter.build_message(
+                self.test_alerter.build_message(
                     alerter.AlertLength.FULL, alerter.AlertType.SUCCESS, m
                 ),
                 textwrap.dedent(
                     """
                     Monitor winning on {hostname} succeeded!
-                    Recovered at: 2020-03-10 09:00:00+00:00
+                    Recovered at: {expected_time}
                     Additional info: 
                     Description: (Monitor did not write an auto-biography.)
                     """.format(  # noqa: W291
-                        hostname=util.short_hostname()
+                        hostname=util.short_hostname(),
+                        expected_time=self.expected_time_string,
                     )
                 ),
             )
 
 
+class TestMessageBuildingTZ(TestMessageBuilding):
+    def setUp(self):
+        super().setUp()
+        self.test_alerter._tz = "Europe/Warsaw"
+        self.expected_time_string = "2020-03-10 10:00:00+01:00"
+
+
 class TestSNSAlerter(unittest.TestCase):
     def test_config(self):
         with self.assertRaises(util.AlerterConfigurationError):

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest coverage flake8 mypy bandit freezegun
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
arrow==1.3.0
bandit==1.8.3
boto3==1.37.23
botocore==1.37.23
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
colorlog==6.9.0
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
flake8==7.2.0
freezegun==1.5.1
idna==3.10
iniconfig==2.1.0
jmespath==1.0.1
markdown-it-py==3.0.0
mccabe==0.7.0
mdurl==0.1.2
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
paho-mqtt==2.1.0
pbr==6.1.1
ping3==4.0.8
pluggy==1.5.0
psutil==7.0.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.1
Pygments==2.19.1
pyOpenSSL==25.0.0
pytest==8.3.5
python-dateutil==2.9.0.post0
PyYAML==6.0.2
requests==2.32.3
rich==14.0.0
s3transfer==0.11.4
-e git+https://github.com/jamesoff/simplemonitor.git@d1204fb82128ffd2b64ecc88ac429966a29e338b#egg=simplemonitor
six==1.17.0
stevedore==5.4.1
tomli==2.2.1
types-python-dateutil==2.9.0.20241206
typing_extensions==4.13.0
urllib3==1.26.20

name: simplemonitor
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - arrow==1.3.0
      - bandit==1.8.3
      - boto3==1.37.23
      - botocore==1.37.23
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - colorlog==6.9.0
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - freezegun==1.5.1
      - idna==3.10
      - iniconfig==2.1.0
      - jmespath==1.0.1
      - markdown-it-py==3.0.0
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - paho-mqtt==2.1.0
      - pbr==6.1.1
      - ping3==4.0.8
      - pluggy==1.5.0
      - psutil==7.0.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyopenssl==25.0.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - requests==2.32.3
      - rich==14.0.0
      - s3transfer==0.11.4
      - six==1.17.0
      - stevedore==5.4.1
      - tomli==2.2.1
      - types-python-dateutil==2.9.0.20241206
      - typing-extensions==4.13.0
      - urllib3==1.26.20
prefix: /opt/conda/envs/simplemonitor

tests/test_alerter.py::TestMessageBuildingTZ::test_full_format_failure tests/test_alerter.py::TestMessageBuildingTZ::test_full_format_failure_docs tests/test_alerter.py::TestMessageBuildingTZ::test_full_format_success tests/test_alerter.py::TestMessageBuildingTZ::test_oneline_format_failure tests/test_alerter.py::TestMessageBuildingTZ::test_sms_format_failure
tests/test_alerter.py::TestMessageBuilding::test_sms_format_success tests/test_alerter.py::TestMessageBuildingTZ::test_sms_format_success
tests/test_alerter.py::TestAlerter::test_allowed_always tests/test_alerter.py::TestAlerter::test_allowed_default tests/test_alerter.py::TestAlerter::test_allowed_not tests/test_alerter.py::TestAlerter::test_allowed_only tests/test_alerter.py::TestAlerter::test_allowed_today tests/test_alerter.py::TestAlerter::test_days tests/test_alerter.py::TestAlerter::test_delay tests/test_alerter.py::TestAlerter::test_dependencies tests/test_alerter.py::TestAlerter::test_dryrun tests/test_alerter.py::TestAlerter::test_groups tests/test_alerter.py::TestAlerter::test_limit tests/test_alerter.py::TestAlerter::test_not_allowed_today tests/test_alerter.py::TestAlerter::test_oohrecovery tests/test_alerter.py::TestAlerter::test_repeat tests/test_alerter.py::TestAlerter::test_should_alert_basic_failure tests/test_alerter.py::TestAlerter::test_should_alert_basic_none tests/test_alerter.py::TestAlerter::test_should_alert_basic_success tests/test_alerter.py::TestAlerter::test_should_alert_catchup tests/test_alerter.py::TestAlerter::test_should_alert_limit tests/test_alerter.py::TestAlerter::test_should_alert_limit_ooh tests/test_alerter.py::TestAlerter::test_should_alert_no_catchup tests/test_alerter.py::TestAlerter::test_should_alert_only_failure tests/test_alerter.py::TestAlerter::test_should_alert_ooh tests/test_alerter.py::TestAlerter::test_should_alert_unavailable tests/test_alerter.py::TestAlerter::test_should_not_alert_basic_success tests/test_alerter.py::TestAlerter::test_should_not_alert_ooh tests/test_alerter.py::TestAlerter::test_times_always tests/test_alerter.py::TestAlerter::test_times_broken tests/test_alerter.py::TestAlerter::test_times_not tests/test_alerter.py::TestAlerter::test_times_only tests/test_alerter.py::TestMessageBuilding::test_full_format_failure tests/test_alerter.py::TestMessageBuilding::test_full_format_failure_docs tests/test_alerter.py::TestMessageBuilding::test_full_format_success tests/test_alerter.py::TestMessageBuilding::test_notification_format_failure tests/test_alerter.py::TestMessageBuilding::test_notification_format_success tests/test_alerter.py::TestMessageBuilding::test_oneline_format_failure tests/test_alerter.py::TestMessageBuilding::test_oneline_format_success tests/test_alerter.py::TestMessageBuilding::test_sms_format_failure tests/test_alerter.py::TestMessageBuildingTZ::test_notification_format_failure tests/test_alerter.py::TestMessageBuildingTZ::test_notification_format_success tests/test_alerter.py::TestMessageBuildingTZ::test_oneline_format_success tests/test_alerter.py::TestSNSAlerter::test_config
.github/workflows/main.yml docs/alerting.md .github/workflows/windows.yml simplemonitor/Alerters/alerter.py
.github/workflows/main.yml docs/alerting.md .github/workflows/windows.yml simplemonitor/Alerters/alerter.py