From e0f49645ec62696a67c031e378fd9e861a8adb8e Mon Sep 17 00:00:00 2001 From: Daniil Pautov Date: Sun, 5 Apr 2026 14:41:45 +0300 Subject: [PATCH] fix: use Z suffix for UTC timestamps and omit null trace_id - LogEntry.__post_init__: use strftime with Z suffix instead of isoformat() which produces +00:00. The server's Zod schema (z.string().datetime()) only accepts the Z suffix. - Normalize caller-supplied +00:00 offset to Z as well. - LogEntry.to_dict(): only include trace_id when it is not None. The server's Zod schema (z.string().optional()) rejects null; only undefined (omitted) or a string value are valid. --- logtide_sdk/models.py | 10 +++++++--- tests/test_v084.py | 45 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/logtide_sdk/models.py b/logtide_sdk/models.py index 3619809..5314ed2 100644 --- a/logtide_sdk/models.py +++ b/logtide_sdk/models.py @@ -31,18 +31,22 @@ class LogEntry: def __post_init__(self) -> None: """Initialize default values.""" if self.time is None: - self.time = datetime.now(timezone.utc).isoformat() + self.time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + elif self.time.endswith("+00:00"): + self.time = self.time[:-6] + "Z" def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for JSON serialization.""" - return { + result: Dict[str, Any] = { "service": self.service, "level": self.level.value, "message": self.message, "time": self.time, "metadata": self.metadata, - "trace_id": self.trace_id, } + if self.trace_id is not None: + result["trace_id"] = self.trace_id + return result @dataclass diff --git a/tests/test_v084.py b/tests/test_v084.py index ed55997..e3882a4 100644 --- a/tests/test_v084.py +++ b/tests/test_v084.py @@ -16,7 +16,6 @@ serialize_exception, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -202,12 +201,48 @@ def test_error_method_uses_exception_key(): # --------------------------------------------------------------------------- -def test_log_entry_time_is_timezone_aware(): - """LogEntry.time must include timezone offset (not naive UTC).""" +def test_log_entry_time_uses_z_suffix(): + """LogEntry.time must use Z suffix for UTC (required by server schema).""" entry = LogEntry(service="svc", level=LogLevel.INFO, message="hello") assert entry.time is not None - # timezone.utc isoformat produces '+00:00' suffix - assert "+00:00" in entry.time or "Z" in entry.time + assert entry.time.endswith("Z") + assert "+00:00" not in entry.time + + +def test_log_entry_normalizes_offset_to_z(): + """Caller-supplied +00:00 offset must be normalized to Z.""" + entry = LogEntry( + service="svc", + level=LogLevel.INFO, + message="hello", + time="2026-04-05T10:00:00.123456+00:00", + ) + assert entry.time == "2026-04-05T10:00:00.123456Z" + + +def test_log_entry_preserves_non_utc_offset(): + """Non-UTC offsets must be left as-is (caller's responsibility).""" + entry = LogEntry( + service="svc", + level=LogLevel.INFO, + message="hello", + time="2026-04-05T10:00:00+03:00", + ) + assert entry.time == "2026-04-05T10:00:00+03:00" + + +def test_to_dict_omits_none_trace_id(): + """to_dict() must omit trace_id when None (server rejects null).""" + entry = LogEntry(service="svc", level=LogLevel.INFO, message="hello") + d = entry.to_dict() + assert "trace_id" not in d + + +def test_to_dict_includes_trace_id_when_set(): + """to_dict() must include trace_id when it has a value.""" + entry = LogEntry(service="svc", level=LogLevel.INFO, message="hello", trace_id="abc-123") + d = entry.to_dict() + assert d["trace_id"] == "abc-123" # ---------------------------------------------------------------------------