From e5aec3a726e76277b72f910b5409013fd7967d93 Mon Sep 17 00:00:00 2001 From: Chris Hager Date: Thu, 21 Nov 2024 20:48:51 +0100 Subject: [PATCH] Log events: allow custom timestamps --- README.md | 27 ++++++++++++++++++++++++--- systemapi/server.go | 19 +++++++++++++++++++ systemapi/server_test.go | 31 ++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe1320b..3ac1f16 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,31 @@ $ curl localhost:3535/logs 2024-10-23T12:04:07Z this is a test ``` - ## Actions +#### Event timestamps - Actions are shell commands that can be executed via API. The commands are defined in the config file, - see [systemapi-config.toml](./systemapi-config.toml) for examples. +By default, events are timestamped with the current time. However, the timestamp can be overridden by +providing a valid unix timestamp (in seconds or milliseconds) as first part of the message: + +```bash +# Start the server +$ go run cmd/system-api/main.go + +# Add regular event +$ echo "hello world" > pipe.fifo + +# Add event with custom timestamp +$ echo "1634966400 this is a test" > pipe.fifo + +# Query events +$ curl localhost:3535/logs +2024-11-21T19:48:04Z hello world +2021-10-23T05:20:00Z this is a test <--- custom timestamp on this entry +``` + +## Actions + +Actions are shell commands that can be executed via API. The commands are defined in the config file, +see [systemapi-config.toml](./systemapi-config.toml) for examples. Actions are recorded in the event log. diff --git a/systemapi/server.go b/systemapi/server.go index c4d5ccf..39b6177 100644 --- a/systemapi/server.go +++ b/systemapi/server.go @@ -12,6 +12,7 @@ import ( "io" "net/http" "os" + "strconv" "strings" "sync" "syscall" @@ -205,6 +206,24 @@ func (s *Server) handleLivenessCheck(w http.ResponseWriter, r *http.Request) { func (s *Server) addEvent(event Event) { // Add event to the list and prune if necessary s.eventsLock.Lock() + + // check for replacement timestamp. if the first part of the message is a timestamp, use it + // instead of the current time. must be a number to be a timestamp (10 characters for seconds, 13 for milliseconds) + eventMsg := event.Message + timestampStr := strings.Fields(eventMsg)[0] + timeInt, err := strconv.ParseInt(timestampStr, 10, 64) + if err == nil { + if len(timestampStr) == 10 { + // timestamp in seconds, update event + event.ReceivedAt = time.Unix(timeInt, 0).UTC() + event.Message = strings.TrimSpace(eventMsg[len(timestampStr):]) + } else if len(timestampStr) == 13 { + // timestamp in milliseconds, update event + event.ReceivedAt = time.UnixMilli(timeInt).UTC() + event.Message = strings.TrimSpace(eventMsg[len(timestampStr):]) + } + } + s.events = append(s.events, event) if len(s.events) > s.cfg.General.LogMaxEntries { s.events = s.events[1:] diff --git a/systemapi/server_test.go b/systemapi/server_test.go index 5db1622..0529add 100644 --- a/systemapi/server_test.go +++ b/systemapi/server_test.go @@ -156,7 +156,7 @@ func TestBasicAuth(t *testing.T) { } func TestMaxEntries(t *testing.T) { - // Verify maximum number of log entries is working correctly + // Ensure maximum number of log entries is working correctly maxEntries := 5 cfg := NewConfig() @@ -177,3 +177,32 @@ func TestMaxEntries(t *testing.T) { require.Equal(t, "4", srv.events[3].Message) require.Equal(t, "5", srv.events[4].Message) } + +func TestAddEntryTimestampParsing(t *testing.T) { + // Ensure that messages with timestamps are correctly parsed + srv := newTestServer(t) + + testTime1 := time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC) + testTime2 := time.Date(2022, 1, 2, 3, 4, 6, 0, time.UTC) + testTime2TimestampSec := testTime2.Unix() + testTime2TimestampMs := testTime2.UnixMilli() + + // Add regular message + srv.addEvent(Event{ReceivedAt: testTime1, Message: "1"}) + + // Add message with timestamp prefix + srv.addEvent(Event{ReceivedAt: testTime1, Message: fmt.Sprintf("%d 2", testTime2TimestampSec)}) + srv.addEvent(Event{ReceivedAt: testTime1, Message: fmt.Sprintf("%d 3", testTime2TimestampMs)}) + + // Check entry 1 (regular message) + require.Equal(t, "1", srv.events[0].Message) + require.Equal(t, testTime1, srv.events[0].ReceivedAt) + + // Check entry 2 (timestamp in seconds) + require.Equal(t, "2", srv.events[1].Message) + require.Equal(t, testTime2, srv.events[1].ReceivedAt) + + // Check entry 3 (timestamp in milliseconds) + require.Equal(t, "3", srv.events[2].Message) + require.Equal(t, testTime2, srv.events[2].ReceivedAt) +}