mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
* Add server side of journal logs This add the server side and grpc methods to get a journal logs from a machine. It repurposes ServiceLogEntry for these logs to keep the changes somewhat to a minimum. And it lets us re-use the merging of the various logs. In the protobufs ContainerLog has been renamed to just Log and LogEntry, as these are now also used for journal logs. It does api.LogOptions in more places to reduce the various logOpts that were used. It does not yet plumb it through to the uc client, that needs a follow up pr. Following logs is also not yet implemented. Signed-off-by: Miek Gieben <miek@miek.nl> * Fix test too Signed-off-by: Miek Gieben <miek@miek.nl> * remove entire comment Signed-off-by: Miek Gieben <miek@miek.nl> * Implement the follow option, untested mind you Signed-off-by: Miek Gieben <miek@miek.nl> * update debug line Signed-off-by: Miek Gieben <miek@miek.nl> * internal/jounal: First batch of PR comments Signed-off-by: Miek Gieben <miek@miek.nl> * internal/journal: code review comments Signed-off-by: Miek Gieben <miek@miek.nl> * Manually apply suggestion Signed-off-by: Miek Gieben <miek@miek.nl> * apply comment manually Signed-off-by: Miek Gieben <miek@miek.nl> * internal/journal: add unit test Signed-off-by: Miek Gieben <miek@miek.nl> * Use testify Signed-off-by: Miek Gieben <miek@miek.nl> * -amFix scanner.Err checking Signed-off-by: Miek Gieben <miek@miek.nl> * Implement code review comments Signed-off-by: Miek Gieben <miek@miek.nl> --------- Signed-off-by: Miek Gieben <miek@miek.nl>
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package client
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/psviderski/uncloud/pkg/api"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testEntry creates a ServiceLogEntry for testing.
|
|
func testEntry(stream api.LogStreamType, ts time.Time, msg string) api.ServiceLogEntry {
|
|
return api.ServiceLogEntry{
|
|
LogEntry: api.LogEntry{
|
|
Stream: stream,
|
|
Timestamp: ts,
|
|
Message: []byte(msg),
|
|
},
|
|
}
|
|
}
|
|
|
|
// collectEntries collects up to maxCount entries from the channel or until it is closed.
|
|
func collectEntries(t *testing.T, ch <-chan api.ServiceLogEntry, maxCount int) []api.ServiceLogEntry {
|
|
t.Helper()
|
|
var entries []api.ServiceLogEntry
|
|
|
|
for i := 0; i < maxCount || maxCount <= 0; i++ {
|
|
select {
|
|
case e, ok := <-ch:
|
|
if !ok {
|
|
return entries
|
|
}
|
|
entries = append(entries, e)
|
|
case <-time.After(1 * time.Second):
|
|
require.FailNow(t, "timed out waiting for log entry")
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
func TestLogMerger_EmptyStreams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
merger := NewLogMerger(nil, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
_, ok := <-output
|
|
assert.False(t, ok, "output channel should be closed for empty streams")
|
|
}
|
|
|
|
func TestLogMerger_SingleStream(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch := make(chan api.ServiceLogEntry, 10)
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
|
|
ch <- testEntry(api.LogStreamStdout, t1, "first")
|
|
ch <- testEntry(api.LogStreamStdout, t2, "second")
|
|
|
|
results := collectEntries(t, output, 2)
|
|
|
|
require.Len(t, results, 2)
|
|
assert.Equal(t, "first", string(results[0].Message))
|
|
assert.Equal(t, "second", string(results[1].Message))
|
|
|
|
close(ch)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
assert.Len(t, results, 0, "no entries expected after close")
|
|
}
|
|
|
|
func TestLogMerger_PreservesData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch := make(chan api.ServiceLogEntry, 10)
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
metadata := api.ServiceLogEntryMetadata{
|
|
ServiceID: "svc-123",
|
|
ServiceName: "my-service",
|
|
MachineID: "machine-456",
|
|
MachineName: "machine-1",
|
|
}
|
|
|
|
e := api.ServiceLogEntry{
|
|
Metadata: metadata,
|
|
LogEntry: api.LogEntry{
|
|
Stream: api.LogStreamStdout,
|
|
Timestamp: time.Now(),
|
|
Message: []byte("test"),
|
|
},
|
|
}
|
|
ch <- e
|
|
close(ch)
|
|
|
|
results := collectEntries(t, output, 0)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, e, results[0])
|
|
}
|
|
|
|
func TestLogMerger_BasicMerge(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch1 := make(chan api.ServiceLogEntry, 10)
|
|
ch2 := make(chan api.ServiceLogEntry, 10)
|
|
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch1, ch2}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
t3 := t1.Add(2 * time.Second)
|
|
t4 := t1.Add(3 * time.Second)
|
|
|
|
// Send interleaved entries from both streams.
|
|
ch1 <- testEntry(api.LogStreamStdout, t1, "ch1-first")
|
|
ch2 <- testEntry(api.LogStreamStdout, t2, "ch2-first")
|
|
ch1 <- testEntry(api.LogStreamStdout, t3, "ch1-second")
|
|
ch2 <- testEntry(api.LogStreamStdout, t4, "ch2-second")
|
|
|
|
// Send heartbeats to advance watermark past all entries.
|
|
t5 := t1.Add(4 * time.Second)
|
|
ch1 <- testEntry(api.LogStreamHeartbeat, t5, "")
|
|
ch2 <- testEntry(api.LogStreamHeartbeat, t5, "")
|
|
|
|
results := collectEntries(t, output, 4)
|
|
require.Len(t, results, 4)
|
|
assert.Equal(t, "ch1-first", string(results[0].Message))
|
|
assert.Equal(t, "ch2-first", string(results[1].Message))
|
|
assert.Equal(t, "ch1-second", string(results[2].Message))
|
|
assert.Equal(t, "ch2-second", string(results[3].Message))
|
|
|
|
close(ch1)
|
|
close(ch2)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
assert.Len(t, results, 1, "one debounced heartbeat expected after close")
|
|
assert.Equal(t, api.LogStreamHeartbeat, results[0].Stream)
|
|
assert.Equal(t, t5, results[0].Timestamp)
|
|
}
|
|
|
|
func TestLogMerger_HeartbeatAdvancesWatermark(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch1 := make(chan api.ServiceLogEntry, 10)
|
|
ch2 := make(chan api.ServiceLogEntry, 10)
|
|
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch1, ch2}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
t3 := t1.Add(2 * time.Second)
|
|
|
|
// Stream 1 sends a log entry.
|
|
ch1 <- testEntry(api.LogStreamStdout, t1, "ch1-first")
|
|
ch1 <- testEntry(api.LogStreamStdout, t3, "ch1-second")
|
|
// Stream 2 is quiet but sends a heartbeat.
|
|
ch2 <- testEntry(api.LogStreamHeartbeat, t2, "")
|
|
|
|
// Now stream 1's entry should be emitted because watermark is t1 (min of t1, t2).
|
|
results := collectEntries(t, output, 1)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "ch1-first", string(results[0].Message))
|
|
|
|
close(ch1)
|
|
close(ch2)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
require.NotEmpty(t, results)
|
|
|
|
// Depending on timing, we may get a heartbeat before the second log entry.
|
|
require.LessOrEqual(t, len(results), 2, "one or two entries expected after close")
|
|
if len(results) == 2 {
|
|
assert.Equal(t, api.LogStreamHeartbeat, results[0].Stream)
|
|
assert.Equal(t, t2, results[0].Timestamp)
|
|
results = results[1:]
|
|
}
|
|
assert.Equal(t, "ch1-second", string(results[0].Message))
|
|
}
|
|
|
|
func TestLogMerger_ErrorForwarding(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch1 := make(chan api.ServiceLogEntry, 10)
|
|
ch2 := make(chan api.ServiceLogEntry, 10)
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch1, ch2}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
t1 := time.Now()
|
|
|
|
ch1 <- testEntry(api.LogStreamStdout, t1, "ch1-first")
|
|
// Send an error entry.
|
|
ch1 <- api.ServiceLogEntry{
|
|
LogEntry: api.LogEntry{
|
|
Err: assert.AnError,
|
|
},
|
|
}
|
|
|
|
results := collectEntries(t, output, 1)
|
|
require.Len(t, results, 1, "expected error to emitted out of order")
|
|
assert.Equal(t, assert.AnError, results[0].Err)
|
|
|
|
close(ch1)
|
|
close(ch2)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, "ch1-first", string(results[0].Message))
|
|
}
|
|
|
|
func TestLogMerger_OutOfOrderSingleStream(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch := make(chan api.ServiceLogEntry, 10)
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
t3 := t1.Add(2 * time.Second)
|
|
t4 := t1.Add(3 * time.Second)
|
|
|
|
// Send entries out of order within the stream.
|
|
ch <- testEntry(api.LogStreamStdout, t3, "third")
|
|
ch <- testEntry(api.LogStreamStdout, t2, "second")
|
|
ch <- testEntry(api.LogStreamStdout, t4, "forth")
|
|
ch <- testEntry(api.LogStreamStdout, t1, "first")
|
|
|
|
results := collectEntries(t, output, 4)
|
|
require.Len(t, results, 4)
|
|
// Emitted in the same order as sent since no buffering/reordering is done within a single stream.
|
|
assert.Equal(t, "third", string(results[0].Message))
|
|
assert.Equal(t, "second", string(results[1].Message))
|
|
assert.Equal(t, "forth", string(results[2].Message))
|
|
assert.Equal(t, "first", string(results[3].Message))
|
|
|
|
close(ch)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
assert.Len(t, results, 0, "no entries expected after close")
|
|
}
|
|
|
|
// Test that entries from streams with different rates are merged correctly in chronological order.
|
|
func TestLogMerger_UnevenStreams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
numFastEntries := logMergerMaxInFlightPerStream
|
|
|
|
// Use buffered channels to allow sends without blocking on receiver.
|
|
ch1 := make(chan api.ServiceLogEntry, numFastEntries+5)
|
|
ch2 := make(chan api.ServiceLogEntry, 10)
|
|
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch1, ch2}, LogMergerOptions{})
|
|
output := merger.Stream()
|
|
|
|
baseTime := time.Now()
|
|
|
|
// Stream 1 sends many entries quickly (0ms - 104ms).
|
|
for i := range numFastEntries {
|
|
ch1 <- testEntry(api.LogStreamStdout, baseTime.Add(time.Duration(i)*time.Millisecond), "fast")
|
|
}
|
|
|
|
// Stream 2 sends entries that interleave with fast entries.
|
|
ch2 <- testEntry(api.LogStreamStdout, baseTime.Add(5*time.Millisecond), "slow")
|
|
ch2 <- testEntry(api.LogStreamStdout, baseTime.Add(50*time.Millisecond), "slow")
|
|
|
|
// Send heartbeats to advance watermark past all entries so they get emitted.
|
|
finalTime := baseTime.Add(time.Second)
|
|
ch1 <- testEntry(api.LogStreamHeartbeat, finalTime, "")
|
|
ch2 <- testEntry(api.LogStreamHeartbeat, finalTime.Add(10*time.Millisecond), "")
|
|
|
|
// Read exactly numFastEntries + 2 log entries.
|
|
expectedCount := numFastEntries + 2
|
|
results := collectEntries(t, output, expectedCount)
|
|
require.Len(t, results, expectedCount)
|
|
|
|
// Count entries from each stream.
|
|
fastCount := 0
|
|
slowCount := 0
|
|
for _, entry := range results {
|
|
switch string(entry.Message) {
|
|
case "fast":
|
|
fastCount++
|
|
case "slow":
|
|
slowCount++
|
|
}
|
|
}
|
|
assert.Equal(t, numFastEntries, fastCount)
|
|
assert.Equal(t, 2, slowCount)
|
|
|
|
// Verify entries are in chronological order.
|
|
for i := 1; i < len(results); i++ {
|
|
assert.False(t, results[i].Timestamp.Before(results[i-1].Timestamp),
|
|
"entry %d (ts=%v) should not be before entry %d (ts=%v)",
|
|
i, results[i].Timestamp, i-1, results[i-1].Timestamp)
|
|
}
|
|
|
|
close(ch1)
|
|
close(ch2)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
require.Len(t, results, 1, "one debounced heartbeat expected after close")
|
|
assert.Equal(t, api.LogStreamHeartbeat, results[0].Stream)
|
|
}
|
|
|
|
func TestLogMerger_StalledStreamExcludedFromWatermark(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ch1 := make(chan api.ServiceLogEntry, 10)
|
|
ch2 := make(chan api.ServiceLogEntry, 10)
|
|
|
|
stallTimeout := 100 * time.Millisecond
|
|
merger := NewLogMerger([]<-chan api.ServiceLogEntry{ch1, ch2}, LogMergerOptions{
|
|
StallTimeout: stallTimeout,
|
|
StallCheckInterval: 20 * time.Millisecond,
|
|
})
|
|
output := merger.Stream()
|
|
|
|
//// Collect all output entries in a separate goroutine.
|
|
//var results []api.ServiceLogEntry
|
|
//doneCollecting := make(chan struct{})
|
|
//go func() {
|
|
// for entry := range output {
|
|
// results = append(results, entry)
|
|
// }
|
|
// close(doneCollecting)
|
|
//}()
|
|
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
t3 := t1.Add(2 * time.Second)
|
|
t4 := t1.Add(3 * time.Second)
|
|
|
|
e1 := testEntry(api.LogStreamStdout, t1, "ch1-entry")
|
|
e1.Metadata = api.ServiceLogEntryMetadata{
|
|
ServiceID: "ch1-svc1",
|
|
ServiceName: "ch1-svcName",
|
|
ContainerID: "ch1-ctr1",
|
|
MachineID: "ch1-machine1",
|
|
MachineName: "ch1-machineName",
|
|
}
|
|
|
|
ch1 <- e1
|
|
ch2 <- testEntry(api.LogStreamStdout, t2, "ch2-first")
|
|
|
|
// Watermark is t1 now so ch1's entry could be collected.
|
|
results := collectEntries(t, output, 1)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "ch1-entry", string(results[0].Message))
|
|
|
|
// Keep pushing to stream 2 so that it does not stall. But it can't emit anything because watermark is stuck at t1.
|
|
time.Sleep(stallTimeout / 2)
|
|
ch2 <- testEntry(api.LogStreamStdout, t3, "ch2-second")
|
|
|
|
// 1/2 + 2/3 = 7/6 > 1, so stream 1 should be considered stalled now.
|
|
time.Sleep(stallTimeout / 3 * 2)
|
|
ch2 <- testEntry(api.LogStreamStdout, t4, "ch2-third")
|
|
|
|
results = collectEntries(t, output, 1)
|
|
require.ErrorIs(t, results[0].Err, api.ErrLogStreamStalled)
|
|
assert.Equal(t, e1.Metadata, results[0].Metadata, "stalled entry should have stream metadata")
|
|
|
|
// Now that stream 1 is marked as stalled and ignored, all in-flight entries from stream 2 are emitted.
|
|
results = collectEntries(t, output, 3)
|
|
require.Len(t, results, 3)
|
|
assert.Equal(t, "ch2-first", string(results[0].Message))
|
|
assert.Equal(t, "ch2-second", string(results[1].Message))
|
|
assert.Equal(t, "ch2-third", string(results[2].Message))
|
|
|
|
close(ch1)
|
|
close(ch2)
|
|
|
|
results = collectEntries(t, output, 0)
|
|
assert.Len(t, results, 0)
|
|
}
|