feat(machine-logs): add server side of journal logs (#282)

* 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>
This commit is contained in:
Miek Gieben
2026-04-08 18:51:52 +10:00
committed by GitHub
parent 4b34c42b76
commit cef047221c
19 changed files with 1204 additions and 862 deletions
+69
View File
@@ -0,0 +1,69 @@
package journal
import (
"bufio"
"context"
"fmt"
"io"
"os/exec"
"github.com/psviderski/uncloud/pkg/api"
)
const journalctl = "journalctl"
var commandContext = exec.CommandContext // overidable for the test
func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.ReadCloser, error) {
args := []string{"-u", unit, "--no-hostname"}
args = append(args, "-n")
if opts.Tail > -1 {
args = append(args, fmt.Sprintf("%d", opts.Tail))
} else {
args = append(args, "all")
}
if opts.Follow {
args = append(args, "-f")
}
args = append(args, "-o")
args = append(args, "short-iso-precise")
if opts.Since != "" {
args = append(args, "-S")
args = append(args, opts.Since)
}
if opts.Until != "" {
args = append(args, "-U")
args = append(args, opts.Until)
}
cmd := commandContext(ctx, journalctl, args...)
p, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
return p, nil
}
// follow synchronously follows the io.Reader, writing each new journal entry to channel.
func follow(ctx context.Context, reader io.Reader, outCh chan api.LogEntry) error {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
select {
case outCh <- entry(scanner.Bytes()):
case <-ctx.Done():
return nil
}
}
if err := scanner.Err(); err != nil {
outCh <- api.LogEntry{Err: fmt.Errorf("journal logs: %w", err)}
}
return nil
}
+83
View File
@@ -0,0 +1,83 @@
package journal
import (
"bufio"
"bytes"
"context"
"fmt"
"slices"
"time"
"github.com/psviderski/uncloud/pkg/api"
)
// Logs streams logs from a service and returns entries via a channel.
func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan api.LogEntry, error) {
// Hard code unit check for now
switch unit {
case "uncloud":
case "corrosion":
case "docker":
default:
return nil, fmt.Errorf("journal logs: invalid unit: %s", unit)
}
reader, err := logs(ctx, unit, opts)
if err != nil {
return nil, err
}
outCh := make(chan api.LogEntry)
switch opts.Follow {
case false:
go func() {
defer close(outCh)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
outCh <- entry(scanner.Bytes())
}
if err := scanner.Err(); err != nil {
outCh <- api.LogEntry{Err: fmt.Errorf("journal logs: %w", err)}
}
}()
case true:
go func() {
defer close(outCh)
err := follow(ctx, reader, outCh)
if err != nil {
outCh <- api.LogEntry{Err: fmt.Errorf("journal logs: %w", err)}
}
}()
}
return outCh, nil
}
func entry(data []byte) api.LogEntry {
// 2025-10-12T11:03:27+02:00 systemd[1]:
timestamp := time.Time{}
message := data
if len(data) > 30 && data[4] == '-' && data[7] == '-' && data[10] == 'T' {
timestampPart, messagePart, found := bytes.Cut(data, []byte(" "))
var err error
if found {
timestamp, err = time.Parse(time.RFC3339Nano, string(timestampPart))
if err != nil {
timestamp = time.Time{}
}
message = messagePart
}
}
return api.LogEntry{
Timestamp: timestamp,
Message: slices.Clone(message), // scanner controls the buffer
Stream: api.LogStreamStdout,
}
}
+51
View File
@@ -0,0 +1,51 @@
package journal
import (
"context"
"os/exec"
"testing"
"time"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogs(t *testing.T) {
t.Parallel()
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch, err := Logs(ctx, "uncloud", api.ServiceLogsOptions{})
require.NoError(t, err)
i := 0
for range ch {
i++
}
assert.Equal(t, i, 6)
cancel()
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
return exec.CommandContext(ctx, "/usr/bin/tail", "-f", "testdata/logs")
}
ctx = context.Background()
ctx, cancel = context.WithCancel(ctx)
go func() { time.Sleep(1 * time.Second); cancel() }()
ch, err = Logs(ctx, "uncloud", api.ServiceLogsOptions{Tail: 3})
require.NoError(t, err)
i = 0
for range ch {
i++
}
assert.Equal(t, i, 6) // still six is hardbeats are not written here.
}
+6
View File
@@ -0,0 +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
2026-01-23T17:19:33.687155+01:00 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)
2026-01-23T17:19:33.687500+01:00 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
2026-01-23T17:19:33.687826+01:00 fedora kernel: apple-dcp 271c00000.dcp: shmem_setup: iova: ffffe8000, buffer: ffff8000817d4000
+298 -46
View File
@@ -10,6 +10,7 @@ import (
status "google.golang.org/genproto/googleapis/rpc/status" status "google.golang.org/genproto/googleapis/rpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
) )
@@ -21,6 +22,58 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type LogEntry_StreamType int32
const (
LogEntry_UNKNOWN LogEntry_StreamType = 0
LogEntry_STDOUT LogEntry_StreamType = 1
LogEntry_STDERR LogEntry_StreamType = 2
LogEntry_HEARTBEAT LogEntry_StreamType = 3
)
// Enum value maps for LogEntry_StreamType.
var (
LogEntry_StreamType_name = map[int32]string{
0: "UNKNOWN",
1: "STDOUT",
2: "STDERR",
3: "HEARTBEAT",
}
LogEntry_StreamType_value = map[string]int32{
"UNKNOWN": 0,
"STDOUT": 1,
"STDERR": 2,
"HEARTBEAT": 3,
}
)
func (x LogEntry_StreamType) Enum() *LogEntry_StreamType {
p := new(LogEntry_StreamType)
*p = x
return p
}
func (x LogEntry_StreamType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (LogEntry_StreamType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_common_proto_enumTypes[0].Descriptor()
}
func (LogEntry_StreamType) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_common_proto_enumTypes[0]
}
func (x LogEntry_StreamType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use LogEntry_StreamType.Descriptor instead.
func (LogEntry_StreamType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{7, 0}
}
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information // Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request. // about the machine that responded to the request.
type Metadata struct { type Metadata struct {
@@ -343,6 +396,150 @@ func (x *IPPrefix) GetBits() uint32 {
return 0 return 0
} }
type LogsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Options for logs retrieval.
Follow bool `protobuf:"varint,2,opt,name=follow,proto3" json:"follow,omitempty"`
Tail int32 `protobuf:"varint,3,opt,name=tail,proto3" json:"tail,omitempty"` // -1 means all
Since string `protobuf:"bytes,4,opt,name=since,proto3" json:"since,omitempty"` // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
Until string `protobuf:"bytes,5,opt,name=until,proto3" json:"until,omitempty"` // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
}
func (x *LogsRequest) Reset() {
*x = LogsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogsRequest) ProtoMessage() {}
func (x *LogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogsRequest.ProtoReflect.Descriptor instead.
func (*LogsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{6}
}
func (x *LogsRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *LogsRequest) GetFollow() bool {
if x != nil {
return x.Follow
}
return false
}
func (x *LogsRequest) GetTail() int32 {
if x != nil {
return x.Tail
}
return 0
}
func (x *LogsRequest) GetSince() string {
if x != nil {
return x.Since
}
return ""
}
func (x *LogsRequest) GetUntil() string {
if x != nil {
return x.Until
}
return ""
}
type LogEntry struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Stream LogEntry_StreamType `protobuf:"varint,1,opt,name=stream,proto3,enum=api.LogEntry_StreamType" json:"stream,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// Log line content. Empty for heartbeat entries.
Message []byte `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *LogEntry) Reset() {
*x = LogEntry{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LogEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogEntry) ProtoMessage() {}
func (x *LogEntry) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogEntry.ProtoReflect.Descriptor instead.
func (*LogEntry) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{7}
}
func (x *LogEntry) GetStream() LogEntry_StreamType {
if x != nil {
return x.Stream
}
return LogEntry_UNKNOWN
}
func (x *LogEntry) GetTimestamp() *timestamppb.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
func (x *LogEntry) GetMessage() []byte {
if x != nil {
return x.Message
}
return nil
}
var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{ var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
@@ -350,32 +547,55 @@ var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01,
0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x72, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53,
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x61, 0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70,
0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65,
0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x42, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04,
0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73,
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x22, 0x75, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18,
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73,
0x69, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10,
0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a,
0x09, 0x48, 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 0x42, 0x37, 0x5a, 0x35,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -390,27 +610,34 @@ func file_internal_machine_api_pb_common_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_common_proto_rawDescData return file_internal_machine_api_pb_common_proto_rawDescData
} }
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_internal_machine_api_pb_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_internal_machine_api_pb_common_proto_goTypes = []any{ var file_internal_machine_api_pb_common_proto_goTypes = []any{
(*Metadata)(nil), // 0: api.Metadata (LogEntry_StreamType)(0), // 0: api.LogEntry.StreamType
(*Empty)(nil), // 1: api.Empty (*Metadata)(nil), // 1: api.Metadata
(*EmptyResponse)(nil), // 2: api.EmptyResponse (*Empty)(nil), // 2: api.Empty
(*IP)(nil), // 3: api.IP (*EmptyResponse)(nil), // 3: api.EmptyResponse
(*IPPort)(nil), // 4: api.IPPort (*IP)(nil), // 4: api.IP
(*IPPrefix)(nil), // 5: api.IPPrefix (*IPPort)(nil), // 5: api.IPPort
(*status.Status)(nil), // 6: google.rpc.Status (*IPPrefix)(nil), // 6: api.IPPrefix
(*LogsRequest)(nil), // 7: api.LogsRequest
(*LogEntry)(nil), // 8: api.LogEntry
(*status.Status)(nil), // 9: google.rpc.Status
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
} }
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{ var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
6, // 0: api.Metadata.status:type_name -> google.rpc.Status 9, // 0: api.Metadata.status:type_name -> google.rpc.Status
0, // 1: api.Empty.metadata:type_name -> api.Metadata 1, // 1: api.Empty.metadata:type_name -> api.Metadata
1, // 2: api.EmptyResponse.messages:type_name -> api.Empty 2, // 2: api.EmptyResponse.messages:type_name -> api.Empty
3, // 3: api.IPPort.ip:type_name -> api.IP 4, // 3: api.IPPort.ip:type_name -> api.IP
3, // 4: api.IPPrefix.ip:type_name -> api.IP 4, // 4: api.IPPrefix.ip:type_name -> api.IP
5, // [5:5] is the sub-list for method output_type 0, // 5: api.LogEntry.stream:type_name -> api.LogEntry.StreamType
5, // [5:5] is the sub-list for method input_type 10, // 6: api.LogEntry.timestamp:type_name -> google.protobuf.Timestamp
5, // [5:5] is the sub-list for extension type_name 7, // [7:7] is the sub-list for method output_type
5, // [5:5] is the sub-list for extension extendee 7, // [7:7] is the sub-list for method input_type
0, // [0:5] is the sub-list for field type_name 7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_common_proto_init() } func init() { file_internal_machine_api_pb_common_proto_init() }
@@ -491,19 +718,44 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*LogsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_common_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*LogEntry); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
NumEnums: 0, NumEnums: 1,
NumMessages: 6, NumMessages: 8,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_internal_machine_api_pb_common_proto_goTypes, GoTypes: file_internal_machine_api_pb_common_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_common_proto_depIdxs, DependencyIndexes: file_internal_machine_api_pb_common_proto_depIdxs,
EnumInfos: file_internal_machine_api_pb_common_proto_enumTypes,
MessageInfos: file_internal_machine_api_pb_common_proto_msgTypes, MessageInfos: file_internal_machine_api_pb_common_proto_msgTypes,
}.Build() }.Build()
File_internal_machine_api_pb_common_proto = out.File File_internal_machine_api_pb_common_proto = out.File
+23
View File
@@ -6,6 +6,7 @@ option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
// Vendored at internal/machine/api/vendor/google/rpc/status.proto. // Vendored at internal/machine/api/vendor/google/rpc/status.proto.
import "google/rpc/status.proto"; import "google/rpc/status.proto";
import "google/protobuf/timestamp.proto";
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information // Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request. // about the machine that responded to the request.
@@ -42,3 +43,25 @@ message IPPrefix {
IP ip = 1; IP ip = 1;
uint32 bits = 2; uint32 bits = 2;
} }
message LogsRequest {
string id = 1;
// Options for logs retrieval.
bool follow = 2;
int32 tail = 3; // -1 means all
string since = 4; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
string until = 5; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
}
message LogEntry {
enum StreamType {
UNKNOWN = 0;
STDOUT = 1;
STDERR = 2;
HEARTBEAT = 3;
}
StreamType stream = 1;
google.protobuf.Timestamp timestamp = 2;
// Log line content. Empty for heartbeat entries.
bytes message = 3;
}
File diff suppressed because it is too large Load Diff
+1 -24
View File
@@ -5,7 +5,6 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "internal/machine/api/pb/common.proto"; import "internal/machine/api/pb/common.proto";
service Docker { service Docker {
@@ -17,7 +16,7 @@ service Docker {
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty); rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse); rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
rpc ContainerLogs(ContainerLogsRequest) returns (stream ContainerLogEntry); rpc ContainerLogs(LogsRequest) returns (stream LogEntry);
rpc PullImage(PullImageRequest) returns (stream JSONMessage); rpc PullImage(PullImageRequest) returns (stream JSONMessage);
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse); rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
@@ -132,28 +131,6 @@ message ExecContainerResponse {
} }
} }
message ContainerLogsRequest {
string container_id = 1;
// Options for logs retrieval.
bool follow = 2;
int32 tail = 3; // -1 means all
string since = 4; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
string until = 5; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
}
message ContainerLogEntry {
enum StreamType {
UNKNOWN = 0;
STDOUT = 1;
STDERR = 2;
HEARTBEAT = 3;
}
StreamType stream = 1;
google.protobuf.Timestamp timestamp = 2;
// Log line content. Empty for heartbeat entries.
bytes message = 3;
}
message PullImageRequest { message PullImageRequest {
string image = 1; string image = 1;
// JSON serialised image.PullOptions. // JSON serialised image.PullOptions.
+9 -9
View File
@@ -52,7 +52,7 @@ type DockerClient interface {
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error) ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error)
ContainerLogs(ctx context.Context, in *ContainerLogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ContainerLogEntry], error) ContainerLogs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogEntry], error)
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's // InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
@@ -149,13 +149,13 @@ func (c *dockerClient) ExecContainer(ctx context.Context, opts ...grpc.CallOptio
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse] type Docker_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse]
func (c *dockerClient) ContainerLogs(ctx context.Context, in *ContainerLogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ContainerLogEntry], error) { func (c *dockerClient) ContainerLogs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogEntry], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ContainerLogs_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ContainerLogs_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &grpc.GenericClientStream[ContainerLogsRequest, ContainerLogEntry]{ClientStream: stream} x := &grpc.GenericClientStream[LogsRequest, LogEntry]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil { if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err return nil, err
} }
@@ -166,7 +166,7 @@ func (c *dockerClient) ContainerLogs(ctx context.Context, in *ContainerLogsReque
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_ContainerLogsClient = grpc.ServerStreamingClient[ContainerLogEntry] type Docker_ContainerLogsClient = grpc.ServerStreamingClient[LogEntry]
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) { func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
@@ -298,7 +298,7 @@ type DockerServer interface {
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
ContainerLogs(*ContainerLogsRequest, grpc.ServerStreamingServer[ContainerLogEntry]) error ContainerLogs(*LogsRequest, grpc.ServerStreamingServer[LogEntry]) error
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's // InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
@@ -343,7 +343,7 @@ func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContain
func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error { func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error {
return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented") return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented")
} }
func (UnimplementedDockerServer) ContainerLogs(*ContainerLogsRequest, grpc.ServerStreamingServer[ContainerLogEntry]) error { func (UnimplementedDockerServer) ContainerLogs(*LogsRequest, grpc.ServerStreamingServer[LogEntry]) error {
return status.Errorf(codes.Unimplemented, "method ContainerLogs not implemented") return status.Errorf(codes.Unimplemented, "method ContainerLogs not implemented")
} }
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error { func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
@@ -516,15 +516,15 @@ func _Docker_ExecContainer_Handler(srv interface{}, stream grpc.ServerStream) er
type Docker_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse] type Docker_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]
func _Docker_ContainerLogs_Handler(srv interface{}, stream grpc.ServerStream) error { func _Docker_ContainerLogs_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(ContainerLogsRequest) m := new(LogsRequest)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
return err return err
} }
return srv.(DockerServer).ContainerLogs(m, &grpc.GenericServerStream[ContainerLogsRequest, ContainerLogEntry]{ServerStream: stream}) return srv.(DockerServer).ContainerLogs(m, &grpc.GenericServerStream[LogsRequest, LogEntry]{ServerStream: stream})
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_ContainerLogsServer = grpc.ServerStreamingServer[ContainerLogEntry] type Docker_ContainerLogsServer = grpc.ServerStreamingServer[LogEntry]
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error { func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(PullImageRequest) m := new(PullImageRequest)
+24 -17
View File
@@ -1146,7 +1146,7 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73,
0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18,
0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x73, 0x32, 0x95, 0x05, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a,
0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69,
0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70,
@@ -1184,11 +1184,14 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x6e, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -1227,6 +1230,8 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*Metadata)(nil), // 19: api.Metadata (*Metadata)(nil), // 19: api.Metadata
(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 21: google.protobuf.Empty (*emptypb.Empty)(nil), // 21: google.protobuf.Empty
(*LogsRequest)(nil), // 22: api.LogsRequest
(*LogEntry)(nil), // 23: api.LogEntry
} }
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig 1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
@@ -1256,17 +1261,19 @@ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
21, // 24: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty 21, // 24: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty
9, // 25: api.Machine.Reset:input_type -> api.ResetRequest 9, // 25: api.Machine.Reset:input_type -> api.ResetRequest
11, // 26: api.Machine.InspectService:input_type -> api.InspectServiceRequest 11, // 26: api.Machine.InspectService:input_type -> api.InspectServiceRequest
2, // 27: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse 22, // 27: api.Machine.MachineLogs:input_type -> api.LogsRequest
4, // 28: api.Machine.InitCluster:output_type -> api.InitClusterResponse 2, // 28: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
21, // 29: api.Machine.JoinCluster:output_type -> google.protobuf.Empty 4, // 29: api.Machine.InitCluster:output_type -> api.InitClusterResponse
8, // 30: api.Machine.Token:output_type -> api.TokenResponse 21, // 30: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
0, // 31: api.Machine.Inspect:output_type -> api.MachineInfo 8, // 31: api.Machine.Token:output_type -> api.TokenResponse
6, // 32: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse 0, // 32: api.Machine.Inspect:output_type -> api.MachineInfo
13, // 33: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse 6, // 33: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse
21, // 34: api.Machine.Reset:output_type -> google.protobuf.Empty 13, // 34: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse
12, // 35: api.Machine.InspectService:output_type -> api.InspectServiceResponse 21, // 35: api.Machine.Reset:output_type -> google.protobuf.Empty
27, // [27:36] is the sub-list for method output_type 12, // 36: api.Machine.InspectService:output_type -> api.InspectServiceResponse
18, // [18:27] is the sub-list for method input_type 23, // 37: api.Machine.MachineLogs:output_type -> api.LogEntry
28, // [28:38] is the sub-list for method output_type
18, // [18:28] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name 18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee 18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name 0, // [0:18] is the sub-list for field type_name
+2
View File
@@ -24,6 +24,8 @@ service Machine {
rpc Reset(ResetRequest) returns (google.protobuf.Empty); rpc Reset(ResetRequest) returns (google.protobuf.Empty);
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse); rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
rpc MachineLogs(LogsRequest) returns (stream LogEntry);
} }
message MachineInfo { message MachineInfo {
+43 -1
View File
@@ -29,6 +29,7 @@ const (
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork" Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork"
Machine_Reset_FullMethodName = "/api.Machine/Reset" Machine_Reset_FullMethodName = "/api.Machine/Reset"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService" Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
Machine_MachineLogs_FullMethodName = "/api.Machine/MachineLogs"
) )
// MachineClient is the client API for Machine service. // MachineClient is the client API for Machine service.
@@ -49,6 +50,7 @@ type MachineClient interface {
// Reset restores the machine to a clean state, removing all cluster-related configuration and data. // Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
MachineLogs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogEntry], error)
} }
type machineClient struct { type machineClient struct {
@@ -149,6 +151,25 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
return out, nil return out, nil
} }
func (c *machineClient) MachineLogs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogEntry], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Machine_ServiceDesc.Streams[0], Machine_MachineLogs_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[LogsRequest, LogEntry]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Machine_MachineLogsClient = grpc.ServerStreamingClient[LogEntry]
// MachineServer is the server API for Machine service. // MachineServer is the server API for Machine service.
// All implementations must embed UnimplementedMachineServer // All implementations must embed UnimplementedMachineServer
// for forward compatibility. // for forward compatibility.
@@ -167,6 +188,7 @@ type MachineServer interface {
// Reset restores the machine to a clean state, removing all cluster-related configuration and data. // Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
MachineLogs(*LogsRequest, grpc.ServerStreamingServer[LogEntry]) error
mustEmbedUnimplementedMachineServer() mustEmbedUnimplementedMachineServer()
} }
@@ -204,6 +226,9 @@ func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptyp
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) { func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented") return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
} }
func (UnimplementedMachineServer) MachineLogs(*LogsRequest, grpc.ServerStreamingServer[LogEntry]) error {
return status.Errorf(codes.Unimplemented, "method MachineLogs not implemented")
}
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {} func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
func (UnimplementedMachineServer) testEmbeddedByValue() {} func (UnimplementedMachineServer) testEmbeddedByValue() {}
@@ -387,6 +412,17 @@ func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Machine_MachineLogs_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(LogsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MachineServer).MachineLogs(m, &grpc.GenericServerStream[LogsRequest, LogEntry]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Machine_MachineLogsServer = grpc.ServerStreamingServer[LogEntry]
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine service. // Machine_ServiceDesc is the grpc.ServiceDesc for Machine service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -431,6 +467,12 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
Handler: _Machine_InspectService_Handler, Handler: _Machine_InspectService_Handler,
}, },
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{
{
StreamName: "MachineLogs",
Handler: _Machine_MachineLogs_Handler,
ServerStreams: true,
},
},
Metadata: "internal/machine/api/pb/machine.proto", Metadata: "internal/machine/api/pb/machine.proto",
} }
+11 -12
View File
@@ -1078,20 +1078,19 @@ const logsHeartbeatInterval = 200 * time.Millisecond
// ContainerLogs streams logs from a container. // ContainerLogs streams logs from a container.
func (s *Server) ContainerLogs( func (s *Server) ContainerLogs(
req *pb.ContainerLogsRequest, stream grpc.ServerStreamingServer[pb.ContainerLogEntry], req *pb.LogsRequest, stream grpc.ServerStreamingServer[pb.LogEntry],
) error { ) error {
// Stream context is cancelled when the client has disconnected or the stream has ended. // Stream context is cancelled when the client has disconnected or the stream has ended.
ctx := stream.Context() ctx := stream.Context()
opts := ContainerLogsOptions{ opts := api.ServiceLogsOptions{
ContainerID: req.ContainerId, Follow: req.Follow,
Follow: req.Follow, Tail: int(req.Tail),
Tail: int(req.Tail), Since: req.Since,
Since: req.Since, Until: req.Until,
Until: req.Until,
} }
logsCh, err := s.service.ContainerLogs(ctx, opts) logsCh, err := s.service.ContainerLogs(ctx, req.Id, opts)
if err != nil { if err != nil {
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
return status.Error(codes.NotFound, err.Error()) return status.Error(codes.NotFound, err.Error())
@@ -1099,7 +1098,7 @@ func (s *Server) ContainerLogs(
return status.Errorf(codes.Internal, "get container logs: %v", err) return status.Errorf(codes.Internal, "get container logs: %v", err)
} }
log := slog.With("container_id", req.ContainerId, "stream_id", fmt.Sprintf("%p", stream)[2:]) log := slog.With("container_id", req.Id, "stream_id", fmt.Sprintf("%p", stream)[2:])
log.Debug("Starting container logs streaming.", log.Debug("Starting container logs streaming.",
"follow", req.Follow, "tail", req.Tail, "since", req.Since, "until", req.Until) "follow", req.Follow, "tail", req.Tail, "since", req.Since, "until", req.Until)
@@ -1127,7 +1126,7 @@ func (s *Server) ContainerLogs(
return status.Error(codes.Internal, entry.Err.Error()) return status.Error(codes.Internal, entry.Err.Error())
} }
pbEntry := &pb.ContainerLogEntry{ pbEntry := &pb.LogEntry{
Stream: api.LogStreamTypeToProto(entry.Stream), Stream: api.LogStreamTypeToProto(entry.Stream),
Timestamp: timestamppb.New(entry.Timestamp), Timestamp: timestamppb.New(entry.Timestamp),
Message: entry.Message, Message: entry.Message,
@@ -1148,8 +1147,8 @@ func (s *Server) ContainerLogs(
// Use the timestamp one heartbeat in the past to be conservative. This reduces the chance of sending // Use the timestamp one heartbeat in the past to be conservative. This reduces the chance of sending
// a timestamp that is greater than a log entry currently being parsed but not yet sent, which would // a timestamp that is greater than a log entry currently being parsed but not yet sent, which would
// cause the client to incorrectly believe it has received all logs up to that point. // cause the client to incorrectly believe it has received all logs up to that point.
heartbeat := &pb.ContainerLogEntry{ heartbeat := &pb.LogEntry{
Stream: pb.ContainerLogEntry_HEARTBEAT, Stream: pb.LogEntry_HEARTBEAT,
Timestamp: timestamppb.New(now.Add(-logsHeartbeatInterval)), Timestamp: timestamppb.New(now.Add(-logsHeartbeatInterval)),
} }
if err = stream.Send(heartbeat); err != nil { if err = stream.Send(heartbeat); err != nil {
+6 -15
View File
@@ -155,18 +155,9 @@ func (s *Service) ListImages(ctx context.Context, opts image.ListOptions) (Image
return imagesResp, nil return imagesResp, nil
} }
// ContainerLogsOptions specifies parameters for ContainerLogs.
type ContainerLogsOptions struct {
ContainerID string
Follow bool
Tail int
Since string
Until string
}
// ContainerLogs streams logs from a container and returns demultiplexed entries via a channel. // ContainerLogs streams logs from a container and returns demultiplexed entries via a channel.
// The channel is closed when streaming completes or context is cancelled. // The channel is closed when streaming completes or context is cancelled.
func (s *Service) ContainerLogs(ctx context.Context, opts ContainerLogsOptions) (<-chan api.ContainerLogEntry, error) { func (s *Service) ContainerLogs(ctx context.Context, containerID string, opts api.ServiceLogsOptions) (<-chan api.LogEntry, error) {
dockerOpts := container.LogsOptions{ dockerOpts := container.LogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
@@ -177,12 +168,12 @@ func (s *Service) ContainerLogs(ctx context.Context, opts ContainerLogsOptions)
Timestamps: true, Timestamps: true,
} }
reader, err := s.Client.ContainerLogs(ctx, opts.ContainerID, dockerOpts) reader, err := s.Client.ContainerLogs(ctx, containerID, dockerOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
outCh := make(chan api.ContainerLogEntry) outCh := make(chan api.LogEntry)
stdoutWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: false} stdoutWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: false}
stderrWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: true} stderrWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: true}
@@ -198,7 +189,7 @@ func (s *Service) ContainerLogs(ctx context.Context, opts ContainerLogsOptions)
if _, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, reader); err != nil { if _, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, reader); err != nil {
// Send error as the last entry. // Send error as the last entry.
select { select {
case outCh <- api.ContainerLogEntry{Err: fmt.Errorf("demultiplex container logs: %w", err)}: case outCh <- api.LogEntry{Err: fmt.Errorf("demultiplex container logs: %w", err)}:
case <-ctx.Done(): case <-ctx.Done():
} }
} }
@@ -216,7 +207,7 @@ func (s *Service) ContainerLogs(ctx context.Context, opts ContainerLogsOptions)
// logsChannelWriter is a writer for stdcopy.StdCopy that sends demultiplexed container logs to a channel. // logsChannelWriter is a writer for stdcopy.StdCopy that sends demultiplexed container logs to a channel.
type logsChannelWriter struct { type logsChannelWriter struct {
ctx context.Context ctx context.Context
ch chan<- api.ContainerLogEntry ch chan<- api.LogEntry
isStderr bool isStderr bool
} }
@@ -236,7 +227,7 @@ func (w *logsChannelWriter) Write(data []byte) (n int, err error) {
} }
} }
entry := api.ContainerLogEntry{ entry := api.LogEntry{
Timestamp: timestamp, Timestamp: timestamp,
// Clone is required because message is a slice into data, which stdcopy.StdCopy may reuse // Clone is required because message is a slice into data, which stdcopy.StdCopy may reuse
// after Write returns but before the entry is consumed from the channel. // after Write returns but before the entry is consumed from the channel.
+94
View File
@@ -14,12 +14,15 @@ import (
"slices" "slices"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/containerd/errdefs"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
"github.com/psviderski/uncloud/internal/corrosion" "github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/internal/docker" "github.com/psviderski/uncloud/internal/docker"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/journal"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy" apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
"github.com/psviderski/uncloud/internal/machine/caddyconfig" "github.com/psviderski/uncloud/internal/machine/caddyconfig"
@@ -30,6 +33,7 @@ import (
machinedocker "github.com/psviderski/uncloud/internal/machine/docker" machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/unregistry" "github.com/psviderski/unregistry"
"github.com/siderolabs/grpc-proxy/proxy" "github.com/siderolabs/grpc-proxy/proxy"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -1073,3 +1077,93 @@ func (m *Machine) InspectService(
} }
return &pb.InspectServiceResponse{Service: svc}, nil return &pb.InspectServiceResponse{Service: svc}, nil
} }
// logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream.
const logsHeartbeatInterval = 200 * time.Millisecond
// MachineLogs streams logs from a systemd service.
func (s *Machine) MachineLogs(
req *pb.LogsRequest, stream grpc.ServerStreamingServer[pb.LogEntry],
) error {
// TODO(miek): almost duplicate of docker/server.ContainerLogs
ctx := stream.Context()
opts := api.ServiceLogsOptions{
Follow: req.Follow,
Tail: int(req.Tail),
Since: req.Since,
Until: req.Until,
}
logsCh, err := journal.Logs(ctx, req.Id, opts)
if err != nil {
if errdefs.IsNotFound(err) {
return status.Error(codes.NotFound, err.Error())
}
return status.Errorf(codes.Internal, "get journal logs: %v", err)
}
log := slog.With("unit", req.Id, "stream_id", fmt.Sprintf("%p", stream)[2:])
log.Debug("Starting systemd service logs streaming.",
"follow", req.Follow, "tail", req.Tail, "since", req.Since, "until", req.Until)
// Heartbeats are needed only when following logs to let the client know when there are no new log entries
// to allow it to advance the watermark of last received log timestamp.
var heartbeatCh <-chan time.Time
if req.Follow {
heartbeatTicker := time.NewTicker(logsHeartbeatInterval)
defer heartbeatTicker.Stop()
heartbeatCh = heartbeatTicker.C
}
started := time.Now()
lastSent := time.Time{}
for {
select {
case entry, ok := <-logsCh:
if !ok {
// Channel closed, no more log entries.
return nil
}
if entry.Err != nil {
return status.Error(codes.Internal, entry.Err.Error())
}
pbEntry := &pb.LogEntry{
Stream: api.LogStreamTypeToProto(entry.Stream),
Timestamp: timestamppb.New(entry.Timestamp),
Message: entry.Message,
}
if err = stream.Send(pbEntry); err != nil {
return status.Errorf(codes.Internal, "send log entry: %v", err)
}
lastSent = entry.Timestamp
case now := <-heartbeatCh:
// Only send heartbeat if no log entries have been sent since the last heartbeat interval or
// if no log entries have been sent at all for at least a heartbeat interval since starting.
if now.Sub(lastSent) < logsHeartbeatInterval ||
(lastSent.IsZero() && now.Sub(started) < logsHeartbeatInterval) {
continue
}
// Use the timestamp one heartbeat in the past to be conservative. This reduces the chance of sending
// a timestamp that is greater than a log entry currently being parsed but not yet sent, which would
// cause the client to incorrectly believe it has received all logs up to that point.
heartbeat := &pb.LogEntry{
Stream: pb.LogEntry_HEARTBEAT,
Timestamp: timestamppb.New(now.Add(-logsHeartbeatInterval)),
}
if err = stream.Send(heartbeat); err != nil {
return status.Errorf(codes.Internal, "send log stream heartbeat: %v", err)
}
lastSent = heartbeat.Timestamp.AsTime()
log.Debug("Sent log stream heartbeat.", "timestamp", lastSent)
case <-ctx.Done():
return status.Error(codes.Canceled, ctx.Err().Error())
}
}
}
+14 -14
View File
@@ -18,31 +18,31 @@ const (
type LogStreamType int type LogStreamType int
// LogStreamTypeFromProto converts a protobuf ContainerLogEntry.StreamType to the internal LogStreamType. // LogStreamTypeFromProto converts a protobuf LogEntry.StreamType to the internal LogStreamType.
func LogStreamTypeFromProto(s pb.ContainerLogEntry_StreamType) LogStreamType { func LogStreamTypeFromProto(s pb.LogEntry_StreamType) LogStreamType {
switch s { switch s {
case pb.ContainerLogEntry_STDOUT: case pb.LogEntry_STDOUT:
return LogStreamStdout return LogStreamStdout
case pb.ContainerLogEntry_STDERR: case pb.LogEntry_STDERR:
return LogStreamStderr return LogStreamStderr
case pb.ContainerLogEntry_HEARTBEAT: case pb.LogEntry_HEARTBEAT:
return LogStreamHeartbeat return LogStreamHeartbeat
default: default:
return LogStreamUnknown return LogStreamUnknown
} }
} }
// LogStreamTypeToProto converts LogStreamType to protobuf ContainerLogEntry.StreamType. // LogStreamTypeToProto converts LogStreamType to protobuf LogEntry.StreamType.
func LogStreamTypeToProto(s LogStreamType) pb.ContainerLogEntry_StreamType { func LogStreamTypeToProto(s LogStreamType) pb.LogEntry_StreamType {
switch s { switch s {
case LogStreamStdout: case LogStreamStdout:
return pb.ContainerLogEntry_STDOUT return pb.LogEntry_STDOUT
case LogStreamStderr: case LogStreamStderr:
return pb.ContainerLogEntry_STDERR return pb.LogEntry_STDERR
case LogStreamHeartbeat: case LogStreamHeartbeat:
return pb.ContainerLogEntry_HEARTBEAT return pb.LogEntry_HEARTBEAT
default: default:
return pb.ContainerLogEntry_UNKNOWN return pb.LogEntry_UNKNOWN
} }
} }
@@ -61,7 +61,7 @@ type ServiceLogsOptions struct {
type ServiceLogEntry struct { type ServiceLogEntry struct {
// Metadata may not be set if an error occurred (Err is not nil). // Metadata may not be set if an error occurred (Err is not nil).
Metadata ServiceLogEntryMetadata Metadata ServiceLogEntryMetadata
ContainerLogEntry LogEntry
} }
// ServiceLogEntryMetadata contains metadata about the source of a log entry. // ServiceLogEntryMetadata contains metadata about the source of a log entry.
@@ -73,8 +73,8 @@ type ServiceLogEntryMetadata struct {
MachineName string MachineName string
} }
// ContainerLogEntry represents a single log entry from a container. // LogEntry represents a single log entry from a container or a service.
type ContainerLogEntry struct { type LogEntry struct {
Stream LogStreamType Stream LogStreamType
Timestamp time.Time Timestamp time.Time
Message []byte Message []byte
+1 -1
View File
@@ -214,7 +214,7 @@ func (m *LogMerger) run() {
for _, s := range stalled { for _, s := range stalled {
errEntry := api.ServiceLogEntry{ errEntry := api.ServiceLogEntry{
ContainerLogEntry: api.ContainerLogEntry{ LogEntry: api.LogEntry{
Err: api.ErrLogStreamStalled, Err: api.ErrLogStreamStalled,
}, },
} }
+3 -3
View File
@@ -12,7 +12,7 @@ import (
// testEntry creates a ServiceLogEntry for testing. // testEntry creates a ServiceLogEntry for testing.
func testEntry(stream api.LogStreamType, ts time.Time, msg string) api.ServiceLogEntry { func testEntry(stream api.LogStreamType, ts time.Time, msg string) api.ServiceLogEntry {
return api.ServiceLogEntry{ return api.ServiceLogEntry{
ContainerLogEntry: api.ContainerLogEntry{ LogEntry: api.LogEntry{
Stream: stream, Stream: stream,
Timestamp: ts, Timestamp: ts,
Message: []byte(msg), Message: []byte(msg),
@@ -91,7 +91,7 @@ func TestLogMerger_PreservesData(t *testing.T) {
e := api.ServiceLogEntry{ e := api.ServiceLogEntry{
Metadata: metadata, Metadata: metadata,
ContainerLogEntry: api.ContainerLogEntry{ LogEntry: api.LogEntry{
Stream: api.LogStreamStdout, Stream: api.LogStreamStdout,
Timestamp: time.Now(), Timestamp: time.Now(),
Message: []byte("test"), Message: []byte("test"),
@@ -199,7 +199,7 @@ func TestLogMerger_ErrorForwarding(t *testing.T) {
ch1 <- testEntry(api.LogStreamStdout, t1, "ch1-first") ch1 <- testEntry(api.LogStreamStdout, t1, "ch1-first")
// Send an error entry. // Send an error entry.
ch1 <- api.ServiceLogEntry{ ch1 <- api.ServiceLogEntry{
ContainerLogEntry: api.ContainerLogEntry{ LogEntry: api.LogEntry{
Err: assert.AnError, Err: assert.AnError,
}, },
} }
+13 -13
View File
@@ -81,18 +81,18 @@ func (cli *Client) ServiceLogs(
// ContainerLogs streams log entries from a single container on a specified machine. // ContainerLogs streams log entries from a single container on a specified machine.
func (cli *Client) ContainerLogs( func (cli *Client) ContainerLogs(
ctx context.Context, machineNameOrID string, containerID string, opts api.ServiceLogsOptions, ctx context.Context, machineNameOrID string, containerID string, opts api.ServiceLogsOptions,
) (<-chan api.ContainerLogEntry, error) { ) (<-chan api.LogEntry, error) {
proxyCtx, _, err := cli.ProxyMachinesContext(ctx, []string{machineNameOrID}) proxyCtx, _, err := cli.ProxyMachinesContext(ctx, []string{machineNameOrID})
if err != nil { if err != nil {
return nil, fmt.Errorf("create request context to proxy to machine '%s': %w", machineNameOrID, err) return nil, fmt.Errorf("create request context to proxy to machine '%s': %w", machineNameOrID, err)
} }
req := &pb.ContainerLogsRequest{ req := &pb.LogsRequest{
ContainerId: containerID, Id: containerID,
Follow: opts.Follow, Follow: opts.Follow,
Tail: int32(opts.Tail), Tail: int32(opts.Tail),
Since: opts.Since, Since: opts.Since,
Until: opts.Until, Until: opts.Until,
} }
if !opts.Follow && opts.Tail == 0 { if !opts.Follow && opts.Tail == 0 {
// If not following and tail is 0, set tail to -1 to return all logs. // If not following and tail is 0, set tail to -1 to return all logs.
@@ -105,7 +105,7 @@ func (cli *Client) ContainerLogs(
return nil, err return nil, err
} }
ch := make(chan api.ContainerLogEntry) ch := make(chan api.LogEntry)
go func() { go func() {
defer close(ch) defer close(ch)
@@ -116,13 +116,13 @@ func (cli *Client) ContainerLogs(
return return
} }
if err != nil { if err != nil {
ch <- api.ContainerLogEntry{ ch <- api.LogEntry{
Err: err, Err: err,
} }
return return
} }
entry := api.ContainerLogEntry{ entry := api.LogEntry{
Stream: api.LogStreamTypeFromProto(pbEntry.Stream), Stream: api.LogStreamTypeFromProto(pbEntry.Stream),
Message: pbEntry.Message, Message: pbEntry.Message,
Timestamp: pbEntry.Timestamp.AsTime(), Timestamp: pbEntry.Timestamp.AsTime(),
@@ -141,15 +141,15 @@ func (cli *Client) ContainerLogs(
// logsStreamWithServiceMetadata wraps a container logs stream and enriches each log entry with service metadata. // logsStreamWithServiceMetadata wraps a container logs stream and enriches each log entry with service metadata.
func logsStreamWithServiceMetadata( func logsStreamWithServiceMetadata(
stream <-chan api.ContainerLogEntry, metadata api.ServiceLogEntryMetadata, stream <-chan api.LogEntry, metadata api.ServiceLogEntryMetadata,
) <-chan api.ServiceLogEntry { ) <-chan api.ServiceLogEntry {
out := make(chan api.ServiceLogEntry) out := make(chan api.ServiceLogEntry)
go func() { go func() {
for entry := range stream { for entry := range stream {
out <- api.ServiceLogEntry{ out <- api.ServiceLogEntry{
Metadata: metadata, Metadata: metadata,
ContainerLogEntry: entry, LogEntry: entry,
} }
} }
close(out) close(out)