mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
fix: machine logs timestamp parsing and streaming for systemd <v255
This commit is contained in:
@@ -47,7 +47,7 @@ func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.Rea
|
|||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-o")
|
args = append(args, "-o")
|
||||||
args = append(args, "short-iso-precise")
|
args = append(args, "short-unix")
|
||||||
|
|
||||||
if opts.Since != "" {
|
if opts.Since != "" {
|
||||||
args = append(args, "-S")
|
args = append(args, "-S")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -33,17 +34,12 @@ func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan
|
|||||||
}
|
}
|
||||||
|
|
||||||
func entry(data []byte) api.LogEntry {
|
func entry(data []byte) api.LogEntry {
|
||||||
// 2025-10-12T11:03:27+02:00 systemd[1]:
|
// 1758193407.686964 systemd[1]: ...
|
||||||
timestamp := time.Time{}
|
timestamp := time.Time{}
|
||||||
message := data
|
message := data
|
||||||
if len(data) > 30 && data[4] == '-' && data[7] == '-' && data[10] == 'T' {
|
if timestampPart, messagePart, found := bytes.Cut(data, []byte(" ")); found {
|
||||||
timestampPart, messagePart, found := bytes.Cut(data, []byte(" "))
|
if t, ok := parseUnixTimestamp(timestampPart); ok {
|
||||||
var err error
|
timestamp = t
|
||||||
if found {
|
|
||||||
timestamp, err = time.Parse(time.RFC3339Nano, string(timestampPart))
|
|
||||||
if err != nil {
|
|
||||||
timestamp = time.Time{}
|
|
||||||
}
|
|
||||||
message = messagePart
|
message = messagePart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,3 +50,20 @@ func entry(data []byte) api.LogEntry {
|
|||||||
Stream: api.LogStreamStdout,
|
Stream: api.LogStreamStdout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseUnixTimestamp parses a journalctl short-unix timestamp "SSSSSSSSSS.UUUUUU" with microsecond precision.
|
||||||
|
func parseUnixTimestamp(b []byte) (time.Time, bool) {
|
||||||
|
secPart, usecPart, found := bytes.Cut(b, []byte("."))
|
||||||
|
if !found {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
sec, err := strconv.ParseInt(string(secPart), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
usec, err := strconv.ParseInt(string(usecPart), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
return time.Unix(sec, usec*1000), true
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,109 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestParseUnixTimestamp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want time.Time
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid with microseconds",
|
||||||
|
input: "1769188773.687500",
|
||||||
|
want: time.Unix(1769188773, 687500000),
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero microseconds",
|
||||||
|
input: "1769188773.000000",
|
||||||
|
want: time.Unix(1769188773, 0),
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "epoch",
|
||||||
|
input: "0.000000",
|
||||||
|
want: time.Unix(0, 0),
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no fractional part",
|
||||||
|
input: "1769188773",
|
||||||
|
want: time.Time{},
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-numeric seconds",
|
||||||
|
input: "abc.000000",
|
||||||
|
want: time.Time{},
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-numeric microseconds",
|
||||||
|
input: "1769188773.abc",
|
||||||
|
want: time.Time{},
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: "",
|
||||||
|
want: time.Time{},
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, ok := parseUnixTimestamp([]byte(tt.input))
|
||||||
|
assert.Equal(t, tt.ok, ok)
|
||||||
|
assert.True(t, tt.want.Equal(got), "want %v, got %v", tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
timestamp time.Time
|
||||||
|
message string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid entry",
|
||||||
|
input: "1769188773.687500 uncloudd[332455]: INFO Starting daemon.",
|
||||||
|
timestamp: time.Unix(1769188773, 687500000),
|
||||||
|
message: "uncloudd[332455]: INFO Starting daemon.\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unparseable timestamp keeps full line as message",
|
||||||
|
input: "-- Boot 1234567890 --",
|
||||||
|
timestamp: time.Time{},
|
||||||
|
message: "-- Boot 1234567890 --\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "line without space keeps full line as message",
|
||||||
|
input: "nospacehere",
|
||||||
|
timestamp: time.Time{},
|
||||||
|
message: "nospacehere\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty line",
|
||||||
|
input: "",
|
||||||
|
timestamp: time.Time{},
|
||||||
|
message: "\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := entry([]byte(tt.input))
|
||||||
|
assert.True(t, tt.timestamp.Equal(got.Timestamp), "timestamp: want %v, got %v", tt.timestamp, got.Timestamp)
|
||||||
|
assert.Equal(t, tt.message, string(got.Message))
|
||||||
|
assert.Equal(t, api.LogStreamStdout, got.Stream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLogs(t *testing.T) {
|
func TestLogs(t *testing.T) {
|
||||||
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
|
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
|
||||||
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
|
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
|
||||||
|
|||||||
Vendored
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
2026-01-23T17:19:33.686964+01:00 fedora kernel: apple-dcp 271c00000.dcp: DCP index:1 dptx target phy: 5 dptx die: 0
|
1769188773.686964 fedora kernel: apple-dcp 271c00000.dcp: DCP index:1 dptx target phy: 5 dptx die: 0
|
||||||
2026-01-23T17:19:33.687155+01:00 fedora kernel: platform 271c00000.dcp:piodma: Adding to iommu group 9
|
1769188773.687155 fedora kernel: platform 271c00000.dcp:piodma: Adding to iommu group 9
|
||||||
2026-01-23T17:19:33.687343+01:00 fedora kernel: apple-dcp 271c00000.dcp: RTKit: Initializing (protocol version 12)
|
1769188773.687343 fedora kernel: apple-dcp 271c00000.dcp: RTKit: Initializing (protocol version 12)
|
||||||
2026-01-23T17:19:33.687500+01:00 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: 880255000 -> pa: be4f29000 -> iomem: ffff800082>
|
1769188773.687500 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: 880255000 -> pa: be4f29000 -> iomem: ffff800082>
|
||||||
2026-01-23T17:19:33.687657+01:00 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: ffffec000, buffer: ffff8000817cc000
|
1769188773.687657 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: ffffec000, buffer: ffff8000817cc000
|
||||||
2026-01-23T17:19:33.687826+01:00 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: ffffe8000, buffer: ffff8000817d4000
|
1769188773.687826 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: ffffe8000, buffer: ffff8000817d4000
|
||||||
|
|||||||
Reference in New Issue
Block a user