Skip to content

Commit e47775d

Browse files
committed
fix: JsonFormatter datetime format to include milliseconds (#156)
1 parent f11e7c5 commit e47775d

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

awslambdaric/lambda_runtime_log_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import json
66
import logging
77
import traceback
8+
from datetime import datetime, timezone
89
from enum import IntEnum
910

10-
_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
11+
_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
1112
_RESERVED_FIELDS = {
1213
"name",
1314
"msg",
@@ -80,6 +81,10 @@ class JsonFormatter(logging.Formatter):
8081
def __init__(self):
8182
super().__init__(datefmt=_DATETIME_FORMAT)
8283

84+
def formatTime(self, record, datefmt=None):
85+
dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
86+
return dt.strftime(_DATETIME_FORMAT + ".%f")[:-3] + "Z"
87+
8388
@staticmethod
8489
def __format_stacktrace(exc_info):
8590
if not exc_info:
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
"""
4+
5+
import logging
6+
import json
7+
import re
8+
import time
9+
import unittest
10+
11+
from awslambdaric.lambda_runtime_log_utils import JsonFormatter
12+
13+
14+
class TestJsonFormatterTimestamp(unittest.TestCase):
15+
def setUp(self):
16+
self.formatter = JsonFormatter()
17+
self.logger = logging.getLogger("test")
18+
self.logger.setLevel(logging.INFO)
19+
20+
def test_timestamp_includes_milliseconds(self):
21+
record = logging.LogRecord(
22+
name="test",
23+
level=logging.INFO,
24+
pathname="test.py",
25+
lineno=1,
26+
msg="hello",
27+
args=None,
28+
exc_info=None,
29+
)
30+
output = self.formatter.format(record)
31+
log_entry = json.loads(output)
32+
timestamp = log_entry["timestamp"]
33+
34+
pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$"
35+
self.assertRegex(
36+
timestamp,
37+
pattern,
38+
f"Timestamp '{timestamp}' does not match expected format YYYY-MM-DDTHH:MM:SS.mmmZ",
39+
)
40+
41+
def test_timestamp_milliseconds_are_accurate(self):
42+
record = logging.LogRecord(
43+
name="test",
44+
level=logging.INFO,
45+
pathname="test.py",
46+
lineno=1,
47+
msg="hello",
48+
args=None,
49+
exc_info=None,
50+
)
51+
record.created = 1718838785.068
52+
output = self.formatter.format(record)
53+
log_entry = json.loads(output)
54+
55+
self.assertEqual(log_entry["timestamp"], "2024-06-19T23:13:05.068Z")
56+
57+
def test_timestamp_zero_milliseconds(self):
58+
record = logging.LogRecord(
59+
name="test",
60+
level=logging.INFO,
61+
pathname="test.py",
62+
lineno=1,
63+
msg="hello",
64+
args=None,
65+
exc_info=None,
66+
)
67+
record.created = 1718838785.0
68+
output = self.formatter.format(record)
69+
log_entry = json.loads(output)
70+
71+
self.assertEqual(log_entry["timestamp"], "2024-06-19T23:13:05.000Z")
72+
73+
def test_timestamps_differ_within_same_second(self):
74+
record1 = logging.LogRecord(
75+
name="test",
76+
level=logging.INFO,
77+
pathname="test.py",
78+
lineno=1,
79+
msg="first",
80+
args=None,
81+
exc_info=None,
82+
)
83+
record1.created = 1718838785.100
84+
85+
record2 = logging.LogRecord(
86+
name="test",
87+
level=logging.INFO,
88+
pathname="test.py",
89+
lineno=1,
90+
msg="second",
91+
args=None,
92+
exc_info=None,
93+
)
94+
record2.created = 1718838785.200
95+
96+
output1 = json.loads(self.formatter.format(record1))
97+
output2 = json.loads(self.formatter.format(record2))
98+
99+
self.assertNotEqual(output1["timestamp"], output2["timestamp"])
100+
self.assertEqual(output1["timestamp"], "2024-06-19T23:13:05.100Z")
101+
self.assertEqual(output2["timestamp"], "2024-06-19T23:13:05.200Z")

0 commit comments

Comments
 (0)