From 0c05f5281f8150ce3684321cda546e65268f6cf1 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:11:56 -0400 Subject: [PATCH] fix(logs): parse date-time log timestamps --- internal/api/client_test.go | 2 +- internal/api/log_stream_test.go | 4 ++- internal/apiclient/client_test.go | 8 +++--- internal/apiclient/common/common.gen.go | 36 ++++++++++++------------- internal/cmd/frontends/logs_test.go | 8 +++--- internal/cmd/functions/logs_test.go | 8 +++--- internal/logfollow/follow_test.go | 6 ++--- internal/output/functions.go | 7 ++--- 8 files changed, 41 insertions(+), 38 deletions(-) diff --git a/internal/api/client_test.go b/internal/api/client_test.go index d52b9c4..3cd3a9a 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -725,7 +725,7 @@ func logsResponse(message string) map[string]any { "data": []any{ map[string]any{ "message": message, - "timestamp": int64(1760000000000), + "timestamp": "2025-10-09T08:53:20Z", }, }, "has_more": false, diff --git a/internal/api/log_stream_test.go b/internal/api/log_stream_test.go index ca957e3..569f747 100644 --- a/internal/api/log_stream_test.go +++ b/internal/api/log_stream_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -31,7 +32,7 @@ func TestStreamProjectLogsRequestAndEvents(t *testing.T) { _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: next-id\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"log-1","message":"build finished","timestamp":1760000000000,"resource":{"type":"function","id":"` + functionID.String() + `"},"deployment":{"id":"` + deploymentID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"log-1","message":"build finished","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"},"deployment":{"id":"` + deploymentID.String() + `"}}` + "\n\n")) _, _ = w.Write([]byte("event: warning\n")) _, _ = w.Write([]byte(`data: {"error":"temporary read failure"}` + "\n\n")) })) @@ -61,6 +62,7 @@ func TestStreamProjectLogsRequestAndEvents(t *testing.T) { assert.Equal(t, "next-id", event.ID) assert.Equal(t, "log-1", event.Log.Id) assert.Equal(t, "build finished", event.Log.Message) + assert.Equal(t, time.Date(2025, 10, 9, 8, 53, 20, 0, time.UTC), event.Log.Timestamp) event, err = stream.Next() require.NoError(t, err) diff --git a/internal/apiclient/client_test.go b/internal/apiclient/client_test.go index 359246a..9114f95 100644 --- a/internal/apiclient/client_test.go +++ b/internal/apiclient/client_test.go @@ -15,8 +15,8 @@ func TestGetProjectLogActivityUsesPostBody(t *testing.T) { resourceIDs := []string{"22222222-2222-4222-8222-222222222222"} levels := []string{"warn"} regions := []string{"us-east-1"} - startTime := int64(1_700_000_000_000) - endTime := int64(1_700_000_300_000) + startTime := "2023-11-14T22:13:20Z" + endTime := "2023-11-14T23:03:20Z" bucketCount := 24 requestBody, err := json.Marshal(map[string]any{ "resource": map[string]any{ @@ -47,7 +47,7 @@ func TestGetProjectLogActivityUsesPostBody(t *testing.T) { assert.Equal(t, []any{resourceIDs[0]}, resource["ids"]) assert.Equal(t, []any{levels[0]}, decodedBody["levels"]) assert.Equal(t, []any{regions[0]}, decodedBody["regions"]) - assert.InEpsilon(t, startTime, decodedBody["start_time"], 0) - assert.InEpsilon(t, endTime, decodedBody["end_time"], 0) + assert.Equal(t, startTime, decodedBody["start_time"]) + assert.Equal(t, endTime, decodedBody["end_time"]) assert.InEpsilon(t, bucketCount, decodedBody["bucket_count"], 0) } diff --git a/internal/apiclient/common/common.gen.go b/internal/apiclient/common/common.gen.go index a42ba1a..d11a603 100644 --- a/internal/apiclient/common/common.gen.go +++ b/internal/apiclient/common/common.gen.go @@ -2043,11 +2043,11 @@ type LogActivityBucket struct { ResourceIds map[string]int `json:"resource_ids"` } `json:"counts"` - // EndTime Bucket end time in milliseconds since epoch. - EndTime int64 `json:"end_time"` + // EndTime Bucket end time. + EndTime time.Time `json:"end_time"` - // StartTime Bucket start time in milliseconds since epoch. - StartTime int64 `json:"start_time"` + // StartTime Bucket start time. + StartTime time.Time `json:"start_time"` // Total Total events in this bucket. Total int `json:"total"` @@ -2058,8 +2058,8 @@ type LogActivityRequest struct { // BucketCount Number of activity buckets to return. BucketCount *int `json:"bucket_count,omitempty"` - // EndTime End time in milliseconds since epoch. - EndTime *int64 `json:"end_time,omitempty"` + // EndTime End time. + EndTime *time.Time `json:"end_time,omitempty"` // Levels Normalized log levels to filter by. If omitted, empty, or all levels are selected, no level filter is applied. Levels *[]LiveLogLevel `json:"levels,omitempty"` @@ -2073,8 +2073,8 @@ type LogActivityRequest struct { // Resource Resource selectors for project log reads. Resource LogRequestResource `json:"resource"` - // StartTime Start time in milliseconds since epoch. - StartTime *int64 `json:"start_time,omitempty"` + // StartTime Start time. + StartTime *time.Time `json:"start_time,omitempty"` } // LogActivityResponse Bucketed runtime log activity. @@ -2144,8 +2144,8 @@ type LogEvent struct { // Resource Resource that owns a historical log event. Resource *LogResource `json:"resource,omitempty"` - // Timestamp Unix timestamp in milliseconds. - Timestamp int64 `json:"timestamp"` + // Timestamp Event timestamp. + Timestamp time.Time `json:"timestamp"` } // LogFrontendRequestResource Frontend log resource selector. @@ -2227,8 +2227,8 @@ type LogSearchEvent struct { // Resource Resource that owns a historical log event. Resource LogResource `json:"resource"` - // Timestamp Unix timestamp in milliseconds. - Timestamp int64 `json:"timestamp"` + // Timestamp Event timestamp. + Timestamp time.Time `json:"timestamp"` } // LogSearchRequest Search request for project logs. @@ -2236,8 +2236,8 @@ type LogSearchRequest struct { // Cursor Opaque pagination cursor from the previous response's `next_cursor`. Cursor *string `json:"cursor,omitempty"` - // EndTime End time in milliseconds since epoch. - EndTime *int64 `json:"end_time,omitempty"` + // EndTime End time. + EndTime *time.Time `json:"end_time,omitempty"` // Levels Normalized log levels to filter by. If omitted, empty, or all levels are selected, no level filter is applied. Levels *[]LiveLogLevel `json:"levels,omitempty"` @@ -2254,8 +2254,8 @@ type LogSearchRequest struct { // Resource Resource selectors for project log reads. Resource LogRequestResource `json:"resource"` - // StartTime Start time in milliseconds since epoch. - StartTime *int64 `json:"start_time,omitempty"` + // StartTime Start time. + StartTime *time.Time `json:"start_time,omitempty"` } // LogSearchResponse Paginated project runtime log search response. @@ -2287,8 +2287,8 @@ type LogStreamRequest struct { // Resource Resource selectors for project log reads. Resource LogRequestResource `json:"resource"` - // StartTime Start time in milliseconds since epoch. - StartTime *int64 `json:"start_time,omitempty"` + // StartTime Start time. + StartTime *time.Time `json:"start_time,omitempty"` } // MetricUsageData Usage data for one metric across totals, daily, and hourly windows. diff --git a/internal/cmd/frontends/logs_test.go b/internal/cmd/frontends/logs_test.go index b6d98c4..a0fa4e3 100644 --- a/internal/cmd/frontends/logs_test.go +++ b/internal/cmd/frontends/logs_test.go @@ -263,7 +263,7 @@ func frontendLogCommandResponse(message string, hasMore bool, next string) map[s map[string]any{ "message": message, "region": "aws-us-east-1", - "timestamp": int64(1760000000000), + "timestamp": "2025-10-09T08:53:20Z", }, }, "has_more": hasMore, @@ -284,13 +284,13 @@ func catchUpLogResponse() map[string]any { "id": "stream-log", "message": "build follow", "region": "aws-us-east-1", - "timestamp": int64(1760000000000), + "timestamp": "2025-10-09T08:53:20Z", }, map[string]any{ "id": "catch-up-log", "message": "catch up log", "region": "aws-us-east-1", - "timestamp": int64(1760000000001), + "timestamp": "2025-10-09T08:53:20.001Z", }, }, "has_more": false, @@ -306,7 +306,7 @@ func writeFrontendLogStream(t *testing.T, w http.ResponseWriter, message string) _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":1760000000000,"resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/cmd/functions/logs_test.go b/internal/cmd/functions/logs_test.go index ef1adcf..7307b2e 100644 --- a/internal/cmd/functions/logs_test.go +++ b/internal/cmd/functions/logs_test.go @@ -274,7 +274,7 @@ func logCommandResponse(message string, hasMore bool, next string) map[string]an map[string]any{ "message": message, "region": "aws-us-east-1", - "timestamp": int64(1760000000000), + "timestamp": "2025-10-09T08:53:20Z", }, }, "has_more": hasMore, @@ -295,13 +295,13 @@ func catchUpLogResponse() map[string]any { "id": "stream-log", "message": "build follow", "region": "aws-us-east-1", - "timestamp": int64(1760000000000), + "timestamp": "2025-10-09T08:53:20Z", }, map[string]any{ "id": "catch-up-log", "message": "catch up log", "region": "aws-us-east-1", - "timestamp": int64(1760000000001), + "timestamp": "2025-10-09T08:53:20.001Z", }, }, "has_more": false, @@ -317,7 +317,7 @@ func writeFunctionLogStream(t *testing.T, w http.ResponseWriter, message string) _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":1760000000000,"resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/logfollow/follow_test.go b/internal/logfollow/follow_test.go index c503457..e0d19e6 100644 --- a/internal/logfollow/follow_test.go +++ b/internal/logfollow/follow_test.go @@ -29,7 +29,7 @@ func TestDeploymentStopsAtTerminalStatusAndRunsCatchUp(t *testing.T) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":1760000000000,"resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) flusher.Flush() <-r.Context().Done() })) @@ -77,7 +77,7 @@ func TestDeploymentRunsCatchUpWhenStreamEndsBeforeTerminal(t *testing.T) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":1760000000000,"resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) })) defer server.Close() @@ -210,7 +210,7 @@ func TestRuntimeSurfacesOpenError(t *testing.T) { func writeStreamLog(w http.ResponseWriter, cursor, logID, message string) { _, _ = w.Write([]byte("id: " + cursor + "\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"` + logID + `","message":"` + message + `","timestamp":1760000000000,"resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"` + logID + `","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/output/functions.go b/internal/output/functions.go index eb0c55a..10d6f60 100644 --- a/internal/output/functions.go +++ b/internal/output/functions.go @@ -108,13 +108,14 @@ func LogSearchEvents(w io.Writer, events []apiclient.LogSearchEvent) { } } -func printLogEvent(w io.Writer, timestamp int64, region *string, message string) { +func printLogEvent(w io.Writer, timestamp time.Time, region *string, message string) { message = strings.TrimSpace(message) + formattedTimestamp := FormatTimestamp(timestamp) if region := stringPtrValue(region); region != "" { - fmt.Fprintf(w, "%s [%s] %s\n", time.UnixMilli(timestamp).Format(time.RFC3339), region, message) + fmt.Fprintf(w, "%s [%s] %s\n", formattedTimestamp, region, message) return } - fmt.Fprintf(w, "%s %s\n", time.UnixMilli(timestamp).Format(time.RFC3339), message) + fmt.Fprintf(w, "%s %s\n", formattedTimestamp, message) } func functionStatus(fn apiclient.Function) string {