127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
type LogSourceCheckpoint struct {
|
|
SourceKey string
|
|
Offset int64
|
|
Sequence uint64
|
|
CursorRef string
|
|
}
|
|
|
|
type LogCheckpointStore interface {
|
|
GetLogCheckpoint(sourceKey string) LogSourceCheckpoint
|
|
PutLogCheckpoint(checkpoint LogSourceCheckpoint)
|
|
}
|
|
|
|
type MemoryLogCheckpointStore struct {
|
|
checkpoints map[string]LogSourceCheckpoint
|
|
}
|
|
|
|
func NewMemoryLogCheckpointStore() *MemoryLogCheckpointStore {
|
|
return &MemoryLogCheckpointStore{checkpoints: map[string]LogSourceCheckpoint{}}
|
|
}
|
|
|
|
func (store *MemoryLogCheckpointStore) GetLogCheckpoint(sourceKey string) LogSourceCheckpoint {
|
|
if store == nil || store.checkpoints == nil {
|
|
return LogSourceCheckpoint{SourceKey: sourceKey}
|
|
}
|
|
return store.checkpoints[sourceKey]
|
|
}
|
|
|
|
func (store *MemoryLogCheckpointStore) PutLogCheckpoint(checkpoint LogSourceCheckpoint) {
|
|
if store == nil {
|
|
return
|
|
}
|
|
if store.checkpoints == nil {
|
|
store.checkpoints = map[string]LogSourceCheckpoint{}
|
|
}
|
|
store.checkpoints[checkpoint.SourceKey] = checkpoint
|
|
}
|
|
|
|
func TailDeclaredFileLogSource(ctx context.Context, workspaceRoot string, assignment protocol.RunJobAssignment, source RuntimeLogSource, sink ProcessLogSink, store LogCheckpointStore) LifecycleExecutionResult {
|
|
if source.Kind != "file.tail" {
|
|
return lifecycleFailure("unsupported_log_source", "only file.tail sources are supported by the local tailer")
|
|
}
|
|
if !protocol.ValidLogicalFileKey(source.Key) || !protocol.ValidLogicalFileKey(source.TargetKey) || !protocol.ValidLogicalFileKey(source.StreamKey) {
|
|
return lifecycleFailure("unsafe_log_source", "log source is unsafe")
|
|
}
|
|
if sink == nil {
|
|
sink = NoopProcessLogSink{}
|
|
}
|
|
if store == nil {
|
|
store = NewMemoryLogCheckpointStore()
|
|
}
|
|
serverRoot, err := scopedServerWorkspace(workspaceRoot, assignment.ServerInstanceID)
|
|
if err != nil {
|
|
return lifecycleFailure("unsafe_log_workspace", err.Error())
|
|
}
|
|
path, err := scopedPath(serverRoot, source.TargetKey)
|
|
if err != nil {
|
|
return lifecycleFailure("unsafe_log_source", err.Error())
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return lifecycleFailure("log_source_open_failed", err.Error())
|
|
}
|
|
defer file.Close()
|
|
|
|
checkpoint := store.GetLogCheckpoint(source.Key)
|
|
if checkpoint.Offset > 0 {
|
|
if _, err := file.Seek(checkpoint.Offset, 0); err != nil {
|
|
return lifecycleFailure("log_source_seek_failed", err.Error())
|
|
}
|
|
}
|
|
reader := bufio.NewReaderSize(file, 64*1024)
|
|
for {
|
|
// A newline only frames an entry. Every other byte, including CR, blank
|
|
// lines, and arbitrarily long output, remains untouched.
|
|
line, readErr := reader.ReadSlice('\n')
|
|
if len(line) > 0 {
|
|
checkpoint.Sequence++
|
|
if err := sink.Append(ctx, assignment, source.StreamKey, strings.TrimSuffix(string(line), "\n")); err != nil {
|
|
return lifecycleFailure("log_source_sink_failed", err.Error())
|
|
}
|
|
checkpoint.SourceKey = source.Key
|
|
checkpoint.Offset += int64(len(line))
|
|
checkpoint.CursorRef = fmt.Sprintf("artifact://jobs/%s/live-log-checkpoint", url.PathEscape(assignment.JobID))
|
|
store.PutLogCheckpoint(checkpoint)
|
|
}
|
|
if readErr == nil || readErr == bufio.ErrBufferFull {
|
|
continue
|
|
}
|
|
if readErr == io.EOF {
|
|
break
|
|
}
|
|
return lifecycleFailure("log_source_read_failed", readErr.Error())
|
|
}
|
|
if checkpoint.CursorRef == "" {
|
|
checkpoint.CursorRef = fmt.Sprintf("artifact://jobs/%s/live-log-checkpoint", url.PathEscape(assignment.JobID))
|
|
}
|
|
return LifecycleExecutionResult{
|
|
State: lifecycleResultStateSucceeded,
|
|
Progress: protocol.RunJobProgressReport{Percent: 100, Message: "live log checkpoint updated"},
|
|
ResultRef: checkpoint.CursorRef,
|
|
Message: "live log source tailed with durable offset checkpoint",
|
|
}
|
|
}
|
|
|
|
func LogCheckpointSummary(checkpoint LogSourceCheckpoint) string {
|
|
return strings.Join([]string{
|
|
"source=" + checkpoint.SourceKey,
|
|
fmt.Sprintf("offset=%d", checkpoint.Offset),
|
|
fmt.Sprintf("sequence=%d", checkpoint.Sequence),
|
|
"cursorRef=" + checkpoint.CursorRef,
|
|
}, " ")
|
|
}
|