Skip to content

Fix access log timestamps ignoring daylight savings time#12085

Merged
Dreamsorcerer merged 13 commits intoaio-libs:masterfrom
nightcityblade:fix/issue-11283
Feb 21, 2026
Merged

Fix access log timestamps ignoring daylight savings time#12085
Dreamsorcerer merged 13 commits intoaio-libs:masterfrom
nightcityblade:fix/issue-11283

Conversation

@nightcityblade
Copy link
Copy Markdown
Contributor

Description

Fixes #11283.

The _format_t method in web_log.py used time.timezone to construct the timezone offset, but time.timezone is a constant representing the standard time UTC offset and does not account for daylight savings time. This causes access log timestamps to show the wrong time and offset during DST periods.

Changes

Replaced the manual timezone construction with datetime.now(tz=datetime.timezone.utc).astimezone(), which correctly resolves to the system's local timezone including DST. Also removed the now-unused import time as time_mod.

Before / After

Before (during MDT, UTC-6):

[08/Jul/2025:12:58:46 -0700]  # Wrong: shows MST offset

After:

[08/Jul/2025:13:58:46 -0600]  # Correct: shows MDT offset

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided There is a change note present in this PR label Feb 17, 2026
Copy link
Copy Markdown
Contributor Author

@nightcityblade nightcityblade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

On the UTC concern: datetime.now(utc).astimezone() does NOT produce UTC timestamps. The .astimezone() call (with no argument) converts to the system's local timezone, correctly reflecting DST. So the flow is: get current time in UTC → convert to local timezone (with DST awareness). The old code used time.timezone which is a module-level constant that never changes, even during DST transitions.

On the test: Added test_access_logger_dst_timezone in 88f273b that simulates an EST→EDT transition and verifies both the UTC offset and hour change correctly. Also added the changelog entry (CHANGES/11283.bugfix.rst).

@Dreamsorcerer
Copy link
Copy Markdown
Member

On the UTC concern: datetime.now(utc).astimezone() does NOT produce UTC timestamps. The .astimezone() call (with no argument) converts to the system's local timezone, correctly reflecting DST. So the flow is: get current time in UTC → convert to local timezone (with DST awareness). The old code used time.timezone which is a module-level constant that never changes, even during DST transitions.

Got it. But, it is creating 2 datetime objects in the process. Do you think my original suggestion could be a better option (assuming it works correctly)? A quick test suggests this version is ~25% slower (and given the frequency of logging, performance is probably fairly important here):

> python3 -m timeit --setup 'import datetime; import time' 'datetime.datetime.now(datetime.timezone(datetime.timedelta(time.localtime().tm_gmtoff)))'
100000 loops, best of 5: 2.02 usec per loop
> python3 -m timeit --setup 'import datetime; import time' 'datetime.datetime.now(datetime.timezone.utc).astimezone()'
100000 loops, best of 5: 2.57 usec per loop

There may be a mistake in my testing, so let me know if you think I'm wrong there.

@Dreamsorcerer
Copy link
Copy Markdown
Member

Dreamsorcerer commented Feb 17, 2026

With my suggestion, the timezone could also be cached, resulting in something that is 4.5x faster:

> python3 -m timeit --setup 'import datetime; import time; tz = datetime.timezone(datetime.timedelta(time.localtime().tm_gmtoff))' 'datetime.datetime.now(tz)'
500000 loops, best of 5: 597 nsec per loop

Obviously, if we cache it, we'd need to have some way to update/invalidate that cache at an appropriate time for when the server is running across a DST change. But, even if we invalidated it every minute, that'd be a substantial performance improvement for a server under load.

@nightcityblade
Copy link
Copy Markdown
Contributor Author

Great benchmarking — you're right that the cached timezone approach is significantly faster. Let me implement that.

I'll update the PR to use a cached datetime.timezone object from time.localtime().tm_gmtoff, with a simple time-based invalidation (e.g. re-check every 60 seconds) to handle DST transitions gracefully. This gives us the ~4.5x speedup while still being correct across DST changes.

Will push the update shortly.

Use time.localtime().tm_gmtoff with a 60-second cache to correctly
resolve the local timezone including DST transitions. The previous
implementation used time.timezone which is a constant and does not
reflect DST changes.

Fixes aio-libs#11283
@nightcityblade
Copy link
Copy Markdown
Contributor Author

Updated! The implementation now uses a cached datetime.timezone built from time.localtime().tm_gmtoff, with a 60-second TTL to handle DST transitions. Also added a dedicated test_access_logger_dst_timezone test that simulates an EST→EDT switch and verifies the offset changes correctly.

Let me know if you'd like any adjustments.

- Use time.time() instead of monotonic clock for cache expiry
- Implement 30-minute thresholds for DST-aware cache invalidation
- Calculate next 30-min boundary aligned to 0/30 minutes past hour
- Reuse datetime.now() calculation between _get_local_timezone and _format_t
- Return tuple from _get_local_timezone to avoid redundant datetime.now() calls

Addresses all 5 inline comments from Dreamsorcerer on PR aio-libs#12085.
@nightcityblade
Copy link
Copy Markdown
Contributor Author

Thank you for the detailed review @Dreamsorcerer! I've addressed all 5 of your inline comments:

1. Use time.time() instead of monotonic clock - Changed from time_mod.monotonic() to time_mod.time() for cache expiry

2. & 3. 30-minute threshold alignment - Implemented your suggested approach for calculating the next 30-minute boundary aligned to DST changes

4. Reuse datetime.now() calculation - Modified _get_local_timezone() to return a tuple of (timezone, datetime) so _format_t() can reuse the datetime calculation instead of calling datetime.now(tz) again

5. Specific code implementation - Used your exact code suggestion for the 30-minute threshold calculation

The implementation now properly aligns cache expiry to 30-minute boundaries (0 and 30 minutes past the hour) to handle DST transitions correctly, and eliminates the redundant datetime.now() calls between timezone caching and formatting.

All existing tests pass with these changes.

@Dreamsorcerer Dreamsorcerer added backport-3.13 Trigger automatic backporting to the 3.13 release branch by Patchback robot backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot labels Feb 20, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 20, 2026

Merging this PR will not alter performance

✅ 59 untouched benchmarks


Comparing nightcityblade:fix/issue-11283 (b51d1c5) with master (51d5dba)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.78%. Comparing base (291d969) to head (b51d1c5).
⚠️ Report is 4 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #12085      +/-   ##
==========================================
+ Coverage   98.71%   98.78%   +0.06%     
==========================================
  Files         128      128              
  Lines       44907    45109     +202     
  Branches     2383     2397      +14     
==========================================
+ Hits        44332    44560     +228     
+ Misses        408      390      -18     
+ Partials      167      159       -8     
Flag Coverage Δ
CI-GHA 98.64% <100.00%> (+0.05%) ⬆️
OS-Linux 98.37% <100.00%> (+0.31%) ⬆️
OS-Windows 96.73% <100.00%> (+0.11%) ⬆️
OS-macOS 97.62% <100.00%> (?)
Py-3.10.11 97.17% <100.00%> (+1.15%) ⬆️
Py-3.10.19 97.65% <100.00%> (?)
Py-3.11.14 97.85% <100.00%> (?)
Py-3.11.9 97.38% <100.00%> (?)
Py-3.12.10 97.47% <100.00%> (?)
Py-3.12.12 97.95% <100.00%> (+0.67%) ⬆️
Py-3.13.11 97.94% <100.00%> (?)
Py-3.13.12 97.45% <100.00%> (+1.15%) ⬆️
Py-3.14.2 97.91% <100.00%> (+0.14%) ⬆️
Py-3.14.3 97.41% <100.00%> (?)
Py-3.14.3t 97.25% <100.00%> (+0.01%) ⬆️
Py-pypy3.11.13-7.3.20 97.28% <12.69%> (?)
VM-macos 97.62% <100.00%> (?)
VM-ubuntu 98.37% <100.00%> (+0.31%) ⬆️
VM-windows 96.73% <100.00%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Dreamsorcerer Dreamsorcerer merged commit 004127a into aio-libs:master Feb 21, 2026
43 checks passed
@patchback
Copy link
Copy Markdown
Contributor

patchback bot commented Feb 21, 2026

Backport to 3.13: 💔 cherry-picking failed — conflicts found

❌ Failed to cleanly apply 004127a on top of patchback/backports/3.13/004127a837d91169f03a40b0904e544c711b481d/pr-12085

Backporting merged PR #12085 into master

  1. Ensure you have a local repo clone of your fork. Unless you cloned it
    from the upstream, this would be your origin remote.
  2. Make sure you have an upstream repo added as a remote too. In these
    instructions you'll refer to it by the name upstream. If you don't
    have it, here's how you can add it:
    $ git remote add upstream https://github.com/aio-libs/aiohttp.git
  3. Ensure you have the latest copy of upstream and prepare a branch
    that will hold the backported code:
    $ git fetch upstream
    $ git checkout -b patchback/backports/3.13/004127a837d91169f03a40b0904e544c711b481d/pr-12085 upstream/3.13
  4. Now, cherry-pick PR Fix access log timestamps ignoring daylight savings time #12085 contents into that branch:
    $ git cherry-pick -x 004127a837d91169f03a40b0904e544c711b481d
    If it'll yell at you with something like fatal: Commit 004127a837d91169f03a40b0904e544c711b481d is a merge but no -m option was given., add -m 1 as follows instead:
    $ git cherry-pick -m1 -x 004127a837d91169f03a40b0904e544c711b481d
  5. At this point, you'll probably encounter some merge conflicts. You must
    resolve them in to preserve the patch from PR Fix access log timestamps ignoring daylight savings time #12085 as close to the
    original as possible.
  6. Push this branch to your fork on GitHub:
    $ git push origin patchback/backports/3.13/004127a837d91169f03a40b0904e544c711b481d/pr-12085
  7. Create a PR, ensure that the CI is green. If it's not — update it so that
    the tests and any other checks pass. This is it!
    Now relax and wait for the maintainers to process your pull request
    when they have some cycles to do reviews. Don't worry — they'll tell you if
    any improvements are necessary when the time comes!

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

@patchback
Copy link
Copy Markdown
Contributor

patchback bot commented Feb 21, 2026

Backport to 3.14: 💔 cherry-picking failed — conflicts found

❌ Failed to cleanly apply 004127a on top of patchback/backports/3.14/004127a837d91169f03a40b0904e544c711b481d/pr-12085

Backporting merged PR #12085 into master

  1. Ensure you have a local repo clone of your fork. Unless you cloned it
    from the upstream, this would be your origin remote.
  2. Make sure you have an upstream repo added as a remote too. In these
    instructions you'll refer to it by the name upstream. If you don't
    have it, here's how you can add it:
    $ git remote add upstream https://github.com/aio-libs/aiohttp.git
  3. Ensure you have the latest copy of upstream and prepare a branch
    that will hold the backported code:
    $ git fetch upstream
    $ git checkout -b patchback/backports/3.14/004127a837d91169f03a40b0904e544c711b481d/pr-12085 upstream/3.14
  4. Now, cherry-pick PR Fix access log timestamps ignoring daylight savings time #12085 contents into that branch:
    $ git cherry-pick -x 004127a837d91169f03a40b0904e544c711b481d
    If it'll yell at you with something like fatal: Commit 004127a837d91169f03a40b0904e544c711b481d is a merge but no -m option was given., add -m 1 as follows instead:
    $ git cherry-pick -m1 -x 004127a837d91169f03a40b0904e544c711b481d
  5. At this point, you'll probably encounter some merge conflicts. You must
    resolve them in to preserve the patch from PR Fix access log timestamps ignoring daylight savings time #12085 as close to the
    original as possible.
  6. Push this branch to your fork on GitHub:
    $ git push origin patchback/backports/3.14/004127a837d91169f03a40b0904e544c711b481d/pr-12085
  7. Create a PR, ensure that the CI is green. If it's not — update it so that
    the tests and any other checks pass. This is it!
    Now relax and wait for the maintainers to process your pull request
    when they have some cycles to do reviews. Don't worry — they'll tell you if
    any improvements are necessary when the time comes!

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

@Dreamsorcerer
Copy link
Copy Markdown
Member

@nightcityblade If you could follow the instructions to create backports, that'd be really helpful.

nightcityblade added a commit to nightcityblade/aiohttp that referenced this pull request Feb 22, 2026
)

---------

Co-authored-by: nightcityblade <[email protected]>
Co-authored-by: Sam Bull <[email protected]>
nightcityblade added a commit to nightcityblade/aiohttp that referenced this pull request Feb 22, 2026
)

---------

Co-authored-by: nightcityblade <[email protected]>
Co-authored-by: Sam Bull <[email protected]>
nightcityblade added a commit to nightcityblade/aiohttp that referenced this pull request Feb 22, 2026
)

---------

Co-authored-by: nightcityblade <[email protected]>
Co-authored-by: Sam Bull <[email protected]>
Dreamsorcerer added a commit that referenced this pull request Feb 22, 2026
nightcityblade pushed a commit to nightcityblade/aiohttp that referenced this pull request Feb 22, 2026
)

Backport of aio-libs#12085 to 3.13 branch. Uses Optional[] syntax for Python 3.9 compatibility.
Dreamsorcerer pushed a commit that referenced this pull request Feb 22, 2026
wavebyrd pushed a commit to wavebyrd/aiohttp that referenced this pull request Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-3.13 Trigger automatic backporting to the 3.13 release branch by Patchback robot backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Access log timestamps do not respect daylight savings time

2 participants