feat(cloud): stream container logs to Dozzle Cloud over gRPC (#4652)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-04-26 07:32:14 -07:00
committed by GitHub
parent e0e061f47c
commit 74fd80229c
32 changed files with 1124 additions and 124 deletions
+1
View File
@@ -131,6 +131,7 @@ declare module 'vue' {
'Mdi:poll': typeof import('~icons/mdi/poll')['default']
'Mdi:refresh': typeof import('~icons/mdi/refresh')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
'Mdi:shieldLockOutline': typeof import('~icons/mdi/shield-lock-outline')['default']
'Mdi:textBoxOutline': typeof import('~icons/mdi/text-box-outline')['default']
'Mdi:trashCanOutline': typeof import('~icons/mdi/trash-can-outline')['default']
'Mdi:webhook': typeof import('~icons/mdi/webhook')['default']
+46
View File
@@ -75,6 +75,25 @@
></progress>
</div>
<label
class="border-base-content/10 hover:border-base-content/20 flex cursor-pointer items-start justify-between gap-4 rounded-lg border p-4 transition-colors"
>
<div class="flex items-start gap-3">
<mdi:shield-lock-outline class="text-primary mt-0.5 shrink-0 text-xl" />
<div class="flex flex-col gap-0.5">
<span class="text-sm font-medium">{{ $t("cloud.stream-logs") }}</span>
<span class="text-base-content/60 text-xs">{{ $t("cloud.stream-logs-help") }}</span>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary toggle-sm shrink-0"
:checked="streamLogs"
:disabled="isSavingStreamLogs"
@change="onStreamLogsChange(($event.target as HTMLInputElement).checked)"
/>
</label>
<div class="flex gap-2">
<a :href="cloudUrl" target="_blank" rel="noreferrer noopener" class="btn btn-sm">
{{ $t("cloud.dashboard") }}
@@ -125,6 +144,33 @@ const {
const isUnlinking = ref(false);
const unlinkModal = ref<HTMLDialogElement | null>(null);
const streamLogs = ref(true);
const isSavingStreamLogs = ref(false);
watchEffect(() => {
if (cloudConfig.value) streamLogs.value = cloudConfig.value.streamLogs;
});
async function onStreamLogsChange(value: boolean | undefined) {
if (!cloudConfig.value || value === undefined) return;
isSavingStreamLogs.value = true;
try {
const res = await fetch(withBase("/api/cloud/config"), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ streamLogs: value }),
});
if (!res.ok) {
streamLogs.value = !value;
return;
}
cloudConfig.value.streamLogs = value;
} catch {
streamLogs.value = !value;
} finally {
isSavingStreamLogs.value = false;
}
}
const usagePercent = computed(() => {
if (!cloudStatus.value) return 0;
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
+1
View File
@@ -71,6 +71,7 @@ export interface CloudConfig {
prefix: string;
expiresAt?: string;
linked: boolean;
streamLogs: boolean;
}
export interface CloudStatus {
+60 -11
View File
@@ -36,16 +36,20 @@ const (
// Client manages the gRPC connection to Dozzle Cloud
type Client struct {
deps ToolDeps
apiKeyFunc func() string
target string
plaintext bool
toolSem *semaphore.Weighted
streamSem *semaphore.Weighted
cachedTools []*pb.ToolDefinition
toolsOnce sync.Once
startCh chan struct{}
activeStreams sync.Map // requestID -> context.CancelFunc
deps ToolDeps
apiKeyFunc func() string
streamLogsFunc func() bool
target string
plaintext bool
toolSem *semaphore.Weighted
streamSem *semaphore.Weighted
cachedTools []*pb.ToolDefinition
toolsOnce sync.Once
startCh chan struct{}
activeStreams sync.Map // requestID -> context.CancelFunc
connMu sync.Mutex
cancelCurrent context.CancelFunc
}
// NewClient creates a new cloud gRPC client.
@@ -82,6 +86,12 @@ func NewClient(apiKeyFunc func() string, deps ToolDeps) *Client {
}
}
// SetStreamLogsFunc registers a function that reports whether bulk container
// log streaming to cloud is enabled. If unset, the streamer runs by default.
func (c *Client) SetStreamLogsFunc(f func() bool) {
c.streamLogsFunc = f
}
// Notify signals the client to attempt a connection. Safe to call multiple times.
// Use this when a cloud dispatcher is added or when the status page is viewed.
func (c *Client) Notify() {
@@ -91,6 +101,17 @@ func (c *Client) Notify() {
}
}
// Reconnect drops the current cloud connection (if any), causing the Run loop
// to dial again so settings like the log-streaming toggle take effect.
func (c *Client) Reconnect() {
c.connMu.Lock()
cancel := c.cancelCurrent
c.connMu.Unlock()
if cancel != nil {
cancel()
}
}
// Run blocks until signaled via Notify(), then connects to the cloud gRPC endpoint
// and processes tool requests. Reconnects automatically on failure.
// Does nothing until Notify() is called — zero overhead for non-cloud users.
@@ -192,8 +213,20 @@ func (c *Client) connect(ctx context.Context, apiKey string) (wasConnected bool,
client := pb.NewCloudToolServiceClient(conn)
// Per-connection context that Reconnect() can cancel to force a redial.
connCtx, connCancel := context.WithCancel(ctx)
defer connCancel()
c.connMu.Lock()
c.cancelCurrent = connCancel
c.connMu.Unlock()
defer func() {
c.connMu.Lock()
c.cancelCurrent = nil
c.connMu.Unlock()
}()
md := metadata.Pairs("x-api-key", apiKey)
streamCtx := metadata.NewOutgoingContext(ctx, md)
streamCtx := metadata.NewOutgoingContext(connCtx, md)
stream, err := client.ToolStream(streamCtx)
if err != nil {
@@ -212,6 +245,22 @@ func (c *Client) connect(ctx context.Context, apiKey string) (wasConnected bool,
return stream.Send(resp)
}
// Start the background log streamer if the host service supports it and
// the user has not opted out via the privacy toggle. Its lifetime is bound
// to streamLifetime — it shuts down cleanly when this connection drops,
// and is re-created on reconnect (re-evaluating the toggle each time).
streamLogs := c.streamLogsFunc == nil || c.streamLogsFunc()
if !streamLogs {
log.Debug().Msg("cloud log streaming disabled by user setting; skipping streamer")
} else if lshs, ok := c.deps.HostService.(LogStreamHostService); ok {
streamer := newLogStreamer(lshs, c.deps.Labels, sendResp)
wg.Go(func() {
streamer.run(streamLifetime)
})
} else {
log.Debug().Msg("host service does not support log streaming; skipping")
}
defer func() {
// Cancel all active log streams before shutting down
c.activeStreams.Range(func(key, value any) bool {
+240
View File
@@ -0,0 +1,240 @@
package cloud
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
pb "github.com/amir20/dozzle/proto/cloud"
"github.com/rs/zerolog/log"
)
// LogStreamHostService is the subset of the host service needed by the log
// streamer. MultiHostService and K8sClusterService both satisfy it.
type LogStreamHostService interface {
ToolHostService
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter)
}
const (
logBatchMaxEntries = 500
logBatchMaxBytes = 256 * 1024
logBatchFlushPeriod = 1 * time.Second
logReaderChanBuffer = 128
)
// logStreamer streams raw container log lines to Dozzle Cloud as unsolicited
// LogBatch ToolResponses. It is created per cloud connection and torn down
// when the connection drops; a new one is started fresh on reconnect.
type logStreamer struct {
hostService LogStreamHostService
labels container.ContainerLabels
send func(resp *pb.ToolResponse) error
mu sync.Mutex
readers map[string]context.CancelFunc
wg sync.WaitGroup
}
func newLogStreamer(hostService LogStreamHostService, labels container.ContainerLabels, send func(resp *pb.ToolResponse) error) *logStreamer {
return &logStreamer{
hostService: hostService,
labels: labels,
send: send,
readers: make(map[string]context.CancelFunc),
}
}
// run blocks until ctx is cancelled. It launches readers for all currently
// running containers and subscribes to new-container events to launch readers
// for containers started after connect.
func (ls *logStreamer) run(ctx context.Context) {
// Subscribe BEFORE snapshotting so we don't miss a container that starts
// between snapshot and subscribe.
started := make(chan container.Container, 64)
ls.hostService.SubscribeContainersStarted(ctx, started, func(_ *container.Container) bool { return true })
existing, errs := ls.hostService.ListAllContainers(ls.labels)
for _, err := range errs {
if err != nil {
log.Debug().Err(err).Msg("log streamer: error listing containers from host")
}
}
for _, c := range existing {
if c.State != "running" {
continue
}
ls.startReader(ctx, c)
}
for {
select {
case <-ctx.Done():
ls.wg.Wait()
return
case c, ok := <-started:
if !ok {
ls.wg.Wait()
return
}
if c.State != "running" {
continue
}
ls.startReader(ctx, c)
}
}
}
func readerKey(hostID, containerID string) string {
return hostID + "|" + containerID
}
func (ls *logStreamer) startReader(parent context.Context, c container.Container) {
key := readerKey(c.Host, c.ID)
ls.mu.Lock()
if _, exists := ls.readers[key]; exists {
ls.mu.Unlock()
return
}
readerCtx, cancel := context.WithCancel(parent)
ls.readers[key] = cancel
ls.mu.Unlock()
cs, err := ls.hostService.FindContainer(c.Host, c.ID, ls.labels)
if err != nil {
ls.mu.Lock()
delete(ls.readers, key)
ls.mu.Unlock()
cancel()
log.Debug().Err(err).Str("container", c.ID).Str("host", c.Host).Msg("log streamer: could not find container, skipping")
return
}
ls.wg.Add(1)
go func() {
defer ls.wg.Done()
defer func() {
ls.mu.Lock()
delete(ls.readers, key)
ls.mu.Unlock()
cancel()
}()
ls.runReader(readerCtx, cs)
}()
}
// runReader follows logs from a single container and pushes batches directly
// to the cloud via ls.send. send() is serialised by the caller, so a slow
// cloud connection backpressures all readers — this is intentional.
func (ls *logStreamer) runReader(ctx context.Context, cs *container_support.ContainerService) {
events := make(chan *container.LogEvent, logReaderChanBuffer)
streamErr := make(chan error, 1)
go func() {
defer close(events)
// Start from "now" to avoid replaying historical logs on every reconnect.
streamErr <- cs.StreamLogs(ctx, time.Now(), container.STDOUT|container.STDERR, events)
}()
hostID := cs.Container.Host
containerID := cs.Container.ID
containerName := cs.Container.Name
log.Debug().Str("container", containerName).Str("host", hostID).Msg("log streamer: reader started")
var batch []*pb.LogBatchEntry
var batchBytes int
flushTicker := time.NewTicker(logBatchFlushPeriod)
defer flushTicker.Stop()
flush := func() error {
if len(batch) == 0 {
return nil
}
err := ls.send(&pb.ToolResponse{Type: &pb.ToolResponse_LogBatch{LogBatch: &pb.LogBatch{Entries: batch}}})
batch = nil
batchBytes = 0
return err
}
defer func() {
_ = flush()
log.Debug().Str("container", containerName).Str("host", hostID).Msg("log streamer: reader stopped")
}()
for {
select {
case <-ctx.Done():
return
case <-flushTicker.C:
if err := flush(); err != nil {
log.Debug().Err(err).Msg("log streamer: send failed")
return
}
case ev, ok := <-events:
if !ok {
if err := <-streamErr; err != nil && ctx.Err() == nil {
log.Debug().Err(err).Str("container", containerName).Msg("log streamer: StreamLogs ended with error")
}
return
}
msg := ev.RawMessage
if msg == "" {
msg = messageToString(ev.Message)
}
tsNs := ev.Timestamp * int64(time.Millisecond) // LogEvent.Timestamp is UnixMilli
if tsNs == 0 {
tsNs = time.Now().UnixNano()
}
level := ev.Level
if level == "unknown" {
level = ""
}
batch = append(batch, &pb.LogBatchEntry{
HostId: hostID,
ContainerId: containerID,
ContainerName: containerName,
TimestampNs: tsNs,
Message: msg,
Stream: ev.Stream,
Level: level,
})
batchBytes += len(msg)
if len(batch) >= logBatchMaxEntries || batchBytes >= logBatchMaxBytes {
if err := flush(); err != nil {
log.Debug().Err(err).Msg("log streamer: send failed")
return
}
}
}
}
}
// messageToString renders a LogEvent.Message of any concrete type into a
// string suitable for transport. Grouped multi-line events don't set
// RawMessage, so JSON-encode their fragment slice as a fallback.
func messageToString(m any) string {
switch v := m.(type) {
case nil:
return ""
case string:
return v
default:
b, err := json.Marshal(v)
if err != nil {
log.Debug().Err(err).Msg("log streamer: failed to marshal message")
return ""
}
return string(b)
}
}
+358
View File
@@ -0,0 +1,358 @@
package cloud
import (
"context"
"io"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
pb "github.com/amir20/dozzle/proto/cloud"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fakeClientService is a ClientService that delivers scripted log events.
type fakeClientService struct {
host container.Host
logsCh chan *container.LogEvent
streamed atomic.Bool
wait chan struct{} // closed once StreamLogs is invoked
once sync.Once
}
func newFakeClientService(hostID string) *fakeClientService {
return &fakeClientService{
host: container.Host{ID: hostID, Name: hostID},
logsCh: make(chan *container.LogEvent, 16),
wait: make(chan struct{}),
}
}
func (f *fakeClientService) FindContainer(_ context.Context, _ string, _ container.ContainerLabels) (container.Container, error) {
return container.Container{}, nil
}
func (f *fakeClientService) ListContainers(_ context.Context, _ container.ContainerLabels) ([]container.Container, error) {
return nil, nil
}
func (f *fakeClientService) Host(_ context.Context) (container.Host, error) { return f.host, nil }
func (f *fakeClientService) ContainerAction(_ context.Context, _ container.Container, _ container.ContainerAction) error {
return nil
}
func (f *fakeClientService) UpdateContainer(_ context.Context, _ container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
close(progressCh)
return false, nil
}
func (f *fakeClientService) LogsBetweenDates(_ context.Context, _ container.Container, _ time.Time, _ time.Time, _ container.StdType) (<-chan *container.LogEvent, error) {
return nil, nil
}
func (f *fakeClientService) RawLogs(_ context.Context, _ container.Container, _ time.Time, _ time.Time, _ container.StdType) (io.ReadCloser, error) {
return nil, nil
}
func (f *fakeClientService) SubscribeStats(_ context.Context, _ chan<- container.ContainerStat) {}
func (f *fakeClientService) SubscribeEvents(_ context.Context, _ chan<- container.ContainerEvent) {}
func (f *fakeClientService) SubscribeContainersStarted(_ context.Context, _ chan<- container.Container) {
}
func (f *fakeClientService) StreamLogs(ctx context.Context, _ container.Container, _ time.Time, _ container.StdType, events chan<- *container.LogEvent) error {
f.once.Do(func() { close(f.wait) })
f.streamed.Store(true)
for {
select {
case <-ctx.Done():
return ctx.Err()
case ev, ok := <-f.logsCh:
if !ok {
return io.EOF
}
select {
case events <- ev:
case <-ctx.Done():
return ctx.Err()
}
}
}
}
func (f *fakeClientService) Attach(_ context.Context, _ container.Container, _ container.ExecEventReader, _ io.Writer) error {
return nil
}
func (f *fakeClientService) Exec(_ context.Context, _ container.Container, _ []string, _ container.ExecEventReader, _ io.Writer) error {
return nil
}
// fakeHostService is a LogStreamHostService driven from a map of containers.
type fakeHostService struct {
mu sync.Mutex
containers []container.Container
clients map[string]*fakeClientService // hostID -> client
startedCh chan<- container.Container
}
func (f *fakeHostService) ListAllContainers(_ container.ContainerLabels) ([]container.Container, []error) {
f.mu.Lock()
defer f.mu.Unlock()
return append([]container.Container(nil), f.containers...), nil
}
func (f *fakeHostService) FindContainer(host string, id string, _ container.ContainerLabels) (*container_support.ContainerService, error) {
f.mu.Lock()
defer f.mu.Unlock()
client, ok := f.clients[host]
if !ok {
return nil, assert.AnError
}
for _, c := range f.containers {
if c.ID == id && c.Host == host {
return container_support.NewContainerService(client, c), nil
}
}
return nil, assert.AnError
}
func (f *fakeHostService) Hosts() []container.Host {
f.mu.Lock()
defer f.mu.Unlock()
hosts := make([]container.Host, 0, len(f.clients))
for id := range f.clients {
hosts = append(hosts, container.Host{ID: id, Name: id})
}
return hosts
}
func (f *fakeHostService) SubscribeContainersStarted(_ context.Context, ch chan<- container.Container, _ container_support.ContainerFilter) {
f.mu.Lock()
f.startedCh = ch
f.mu.Unlock()
}
func (f *fakeHostService) emitStart(c container.Container) {
f.mu.Lock()
f.containers = append(f.containers, c)
ch := f.startedCh
f.mu.Unlock()
if ch != nil {
ch <- c
}
}
func collectBatches(t *testing.T, mu *sync.Mutex, out *[]*pb.LogBatch, wantEntries int, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
mu.Lock()
total := 0
for _, b := range *out {
total += len(b.Entries)
}
mu.Unlock()
if total >= wantEntries {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("timed out waiting for %d entries", wantEntries)
}
func TestLogStreamer_InitialSnapshotAndBatching(t *testing.T) {
client := newFakeClientService("host-1")
hs := &fakeHostService{
containers: []container.Container{
{ID: "c1", Name: "nginx", Host: "host-1", State: "running"},
},
clients: map[string]*fakeClientService{"host-1": client},
}
var sendMu sync.Mutex
var sent []*pb.LogBatch
send := func(resp *pb.ToolResponse) error {
sendMu.Lock()
defer sendMu.Unlock()
if lb := resp.GetLogBatch(); lb != nil {
sent = append(sent, lb)
}
return nil
}
ls := newLogStreamer(hs, nil, send)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runDone := make(chan struct{})
go func() {
ls.run(ctx)
close(runDone)
}()
// Wait for reader to hook in.
select {
case <-client.wait:
case <-time.After(2 * time.Second):
t.Fatal("StreamLogs was never called")
}
ts := time.Now().UnixMilli()
for i := 0; i < 3; i++ {
client.logsCh <- &container.LogEvent{
Timestamp: ts,
RawMessage: "hello world",
Stream: "stdout",
Level: "info",
}
}
collectBatches(t, &sendMu, &sent, 3, 3*time.Second)
sendMu.Lock()
var allEntries []*pb.LogBatchEntry
for _, b := range sent {
allEntries = append(allEntries, b.Entries...)
}
sendMu.Unlock()
require.GreaterOrEqual(t, len(allEntries), 3)
e := allEntries[0]
assert.Equal(t, "host-1", e.HostId)
assert.Equal(t, "c1", e.ContainerId)
assert.Equal(t, "nginx", e.ContainerName)
assert.Equal(t, "hello world", e.Message)
assert.Equal(t, "stdout", e.Stream)
assert.Equal(t, "info", e.Level)
assert.Equal(t, ts*int64(time.Millisecond), e.TimestampNs)
cancel()
<-runDone
}
func TestLogStreamer_NewContainerStartsReader(t *testing.T) {
client := newFakeClientService("host-1")
hs := &fakeHostService{
containers: nil,
clients: map[string]*fakeClientService{"host-1": client},
}
send := func(_ *pb.ToolResponse) error { return nil }
ls := newLogStreamer(hs, nil, send)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runDone := make(chan struct{})
go func() {
ls.run(ctx)
close(runDone)
}()
// Wait for run() to subscribe before emitting; emitting before
// startedCh is registered would silently no-op.
require.Eventually(t, func() bool {
hs.mu.Lock()
defer hs.mu.Unlock()
return hs.startedCh != nil
}, 2*time.Second, 5*time.Millisecond, "subscription was never registered")
hs.emitStart(container.Container{ID: "c-new", Name: "redis", Host: "host-1", State: "running"})
select {
case <-client.wait:
case <-time.After(2 * time.Second):
t.Fatal("reader was not started for new container")
}
cancel()
<-runDone
}
func TestLogStreamer_LevelUnknownIsBlank(t *testing.T) {
client := newFakeClientService("host-1")
hs := &fakeHostService{
containers: []container.Container{
{ID: "c1", Name: "n", Host: "host-1", State: "running"},
},
clients: map[string]*fakeClientService{"host-1": client},
}
var sendMu sync.Mutex
var sent []*pb.LogBatch
send := func(resp *pb.ToolResponse) error {
sendMu.Lock()
defer sendMu.Unlock()
if lb := resp.GetLogBatch(); lb != nil {
sent = append(sent, lb)
}
return nil
}
ls := newLogStreamer(hs, nil, send)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go ls.run(ctx)
<-client.wait
client.logsCh <- &container.LogEvent{Timestamp: time.Now().UnixMilli(), RawMessage: "m", Stream: "stdout", Level: "unknown"}
collectBatches(t, &sendMu, &sent, 1, 2*time.Second)
sendMu.Lock()
assert.Equal(t, "", sent[0].Entries[0].Level)
sendMu.Unlock()
}
func TestLogStreamer_BatchFlushesOnMaxEntries(t *testing.T) {
client := newFakeClientService("host-1")
hs := &fakeHostService{
containers: []container.Container{
{ID: "c1", Name: "n", Host: "host-1", State: "running"},
},
clients: map[string]*fakeClientService{"host-1": client},
}
var sendMu sync.Mutex
var sent []*pb.LogBatch
send := func(resp *pb.ToolResponse) error {
sendMu.Lock()
defer sendMu.Unlock()
if lb := resp.GetLogBatch(); lb != nil {
sent = append(sent, lb)
}
return nil
}
ls := newLogStreamer(hs, nil, send)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runDone := make(chan struct{})
go func() { ls.run(ctx); close(runDone) }()
<-client.wait
// Push just over the max entries cap so we flush before the 1s timer.
ts := time.Now().UnixMilli()
total := logBatchMaxEntries + 10
for i := 0; i < total; i++ {
client.logsCh <- &container.LogEvent{Timestamp: ts, RawMessage: "x", Stream: "stdout", Level: "info"}
}
// A full flush should happen well under 1s (we're below the timer).
deadline := time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
sendMu.Lock()
hasFullBatch := false
for _, b := range sent {
if len(b.Entries) >= logBatchMaxEntries {
hasFullBatch = true
break
}
}
sendMu.Unlock()
if hasFullBatch {
cancel()
<-runDone
return
}
time.Sleep(10 * time.Millisecond)
}
cancel()
<-runDone
t.Fatal("expected a batch with >= logBatchMaxEntries entries")
}
+13
View File
@@ -12,6 +12,19 @@ type CloudConfig struct {
APIKey string `yaml:"apiKey"`
Prefix string `yaml:"prefix"`
ExpiresAt *time.Time `yaml:"expiresAt,omitempty"`
// StreamLogs controls whether container logs are streamed to Dozzle Cloud.
// nil means default (enabled) — preserves behavior for configs written
// before this field existed.
StreamLogs *bool `yaml:"streamLogs,omitempty"`
}
// StreamLogsEnabled reports whether the bulk log stream to cloud should run.
// Defaults to true when the field is unset or the config is nil.
func (c *CloudConfig) StreamLogsEnabled() bool {
if c == nil || c.StreamLogs == nil {
return true
}
return *c.StreamLogs
}
// WriteCloudConfig encodes the given CloudConfig to the writer in YAML format.
+14
View File
@@ -113,6 +113,20 @@ func (p *Persister) SetCloudConfig(cc *CloudConfig) {
p.SaveCloud()
}
// SetCloudStreamLogs updates the StreamLogs flag on the current cloud config
// and persists it. No-op when no cloud config is set.
func (p *Persister) SetCloudStreamLogs(enabled bool) {
p.mu.Lock()
if p.cloudConfig == nil {
p.mu.Unlock()
return
}
v := enabled
p.cloudConfig.StreamLogs = &v
p.mu.Unlock()
p.SaveCloud()
}
// RemoveCloudConfig clears the cloud config, clears the cloud dispatcher, and
// removes the cloud config file from disk.
func (p *Persister) RemoveCloudConfig() {
@@ -248,6 +248,13 @@ func (m *MultiHostService) SetCloudConfig(cc *notification.CloudConfig) {
m.broadcastCloudConfig()
}
// SetCloudStreamLogs updates the bulk-log-streaming privacy flag on the cloud
// config and persists it. Only affects the local cloud client; agents never
// stream logs directly to cloud.
func (m *MultiHostService) SetCloudStreamLogs(enabled bool) {
m.persister.SetCloudStreamLogs(enabled)
}
// RemoveCloudConfig clears the cloud config, removes the cloud dispatcher, deletes the file,
// and broadcasts the change to all agents so they stop sending to cloud.
func (m *MultiHostService) RemoveCloudConfig() {
@@ -231,6 +231,10 @@ func (m *K8sClusterService) SetCloudConfig(cc *notification.CloudConfig) {
m.persister.SetCloudConfig(cc)
}
func (m *K8sClusterService) SetCloudStreamLogs(enabled bool) {
m.persister.SetCloudStreamLogs(enabled)
}
func (m *K8sClusterService) RemoveCloudConfig() {
m.persister.RemoveCloudConfig()
}
+36 -5
View File
@@ -180,9 +180,10 @@ func (h *handler) cloudStatus(w http.ResponseWriter, r *http.Request) {
}
type cloudConfigResponse struct {
Prefix string `json:"prefix"`
ExpiresAt *string `json:"expiresAt,omitempty"`
Linked bool `json:"linked"`
Prefix string `json:"prefix"`
ExpiresAt *string `json:"expiresAt,omitempty"`
Linked bool `json:"linked"`
StreamLogs bool `json:"streamLogs"`
}
func (h *handler) cloudConfig(w http.ResponseWriter, r *http.Request) {
@@ -193,8 +194,9 @@ func (h *handler) cloudConfig(w http.ResponseWriter, r *http.Request) {
}
resp := cloudConfigResponse{
Prefix: cc.Prefix,
Linked: true,
Prefix: cc.Prefix,
Linked: true,
StreamLogs: cc.StreamLogsEnabled(),
}
if cc.ExpiresAt != nil {
s := cc.ExpiresAt.Format(time.RFC3339)
@@ -206,6 +208,35 @@ func (h *handler) cloudConfig(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(resp)
}
type updateCloudConfigRequest struct {
StreamLogs *bool `json:"streamLogs,omitempty"`
}
func (h *handler) updateCloudConfig(w http.ResponseWriter, r *http.Request) {
cc := h.hostService.CloudConfig()
if cc == nil {
writeError(w, http.StatusNotFound, "no cloud configuration")
return
}
var req updateCloudConfigRequest
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<10)).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.StreamLogs != nil {
h.hostService.SetCloudStreamLogs(*req.StreamLogs)
// Drop the active cloud connection so the new flag is picked up on
// the next dial — a streamer may need to start or stop.
if h.config.OnCloudUpdate != nil {
h.config.OnCloudUpdate()
}
}
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) deleteCloudConfig(w http.ResponseWriter, r *http.Request) {
h.hostService.RemoveCloudConfig()
w.WriteHeader(http.StatusNoContent)
+3
View File
@@ -51,6 +51,7 @@ type Config struct {
ReleaseCheckMode ReleaseCheckMode
Labels container.ContainerLabels
OnCloudSetup func()
OnCloudUpdate func()
}
type Authorization struct {
@@ -90,6 +91,7 @@ type HostService interface {
FetchAgentNotificationStats() map[int]types.SubscriptionStats
CloudConfig() *notification.CloudConfig
SetCloudConfig(cc *notification.CloudConfig)
SetCloudStreamLogs(enabled bool)
RemoveCloudConfig()
}
@@ -190,6 +192,7 @@ func createRouter(h *handler) *chi.Mux {
// Cloud API
r.Get("/cloud/status", h.cloudStatus)
r.Get("/cloud/config", h.cloudConfig)
r.Patch("/cloud/config", h.updateCloudConfig)
r.Delete("/cloud/config", h.deleteCloudConfig)
r.Post("/cloud/feedback", h.cloudFeedback)
})
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Fjern tilknytning
unlink-confirm: Er du sikker på, at du vil fjerne tilknytningen til Dozzle Cloud? Dette fjerner alle cloud-notifikationsdestinationer.
stream-logs: Stream containerlogs til Dozzle Cloud
stream-logs-help: Påkrævet for AI-drevne undersøgelser og logsøgning. Deaktiver for at holde alt logindhold på denne instans.
welcome:
title: "Din instans er forbundet!"
subtitle: "Du er klar til at modtage advarsler, daglige resuméer og mere fra Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Verknüpfung aufheben
unlink-confirm: Sind Sie sicher, dass Sie die Verknüpfung mit Dozzle Cloud aufheben möchten? Dies entfernt alle Cloud-Benachrichtigungsziele.
stream-logs: Container-Logs an Dozzle Cloud streamen
stream-logs-help: Erforderlich für KI-gestützte Analysen und Log-Suche. Deaktivieren, um sämtliche Log-Inhalte auf dieser Instanz zu behalten.
welcome:
title: "Ihre Instanz ist verbunden!"
subtitle: "Sie können jetzt Benachrichtigungen, tägliche Zusammenfassungen und mehr von Dozzle Cloud erhalten."
+2
View File
@@ -317,6 +317,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Unlink
unlink-confirm: Are you sure you want to unlink from Dozzle Cloud? This will remove all cloud notification destinations.
stream-logs: Stream container logs to Dozzle Cloud
stream-logs-help: Required for AI-powered investigations and log search. Disable to keep all log content on this instance.
welcome:
title: "Your instance is connected!"
subtitle: "You're all set to start getting alerts, daily digests, and more from Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Desvincular
unlink-confirm: ¿Está seguro de que desea desvincular de Dozzle Cloud? Esto eliminará todos los destinos de notificación en la nube.
stream-logs: Transmitir registros de contenedores a Dozzle Cloud
stream-logs-help: Necesario para investigaciones con IA y búsqueda de registros. Desactívelo para mantener todo el contenido de los registros en esta instancia.
welcome:
title: "¡Tu instancia está conectada!"
subtitle: "Ya puedes recibir alertas, resúmenes diarios y más desde Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Délier
unlink-confirm: Êtes-vous sûr de vouloir délier de Dozzle Cloud ? Cela supprimera toutes les destinations de notification cloud.
stream-logs: Diffuser les journaux de conteneurs vers Dozzle Cloud
stream-logs-help: Requis pour les investigations alimentées par IA et la recherche dans les journaux. Désactivez pour conserver tout le contenu des journaux sur cette instance.
welcome:
title: "Votre instance est connectée !"
subtitle: "Vous êtes prêt à recevoir des alertes, des résumés quotidiens et bien plus depuis Dozzle Cloud."
+2
View File
@@ -316,6 +316,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Lepaskan tautan
unlink-confirm: Apakah Anda yakin ingin melepaskan tautan dari Dozzle Cloud? Ini akan menghapus semua tujuan notifikasi cloud.
stream-logs: Streaming log kontainer ke Dozzle Cloud
stream-logs-help: Diperlukan untuk investigasi bertenaga AI dan pencarian log. Nonaktifkan untuk menyimpan seluruh konten log di instance ini.
welcome:
title: "Instans Anda terhubung!"
subtitle: "Anda siap menerima peringatan, ringkasan harian, dan lainnya dari Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Scollega
unlink-confirm: Sei sicuro di voler scollegare da Dozzle Cloud? Questo rimuoverà tutte le destinazioni di notifica cloud.
stream-logs: Invia i log dei container a Dozzle Cloud
stream-logs-help: Necessario per le indagini basate sull'IA e la ricerca nei log. Disattiva per mantenere tutto il contenuto dei log su questa istanza.
welcome:
title: "La tua istanza è connessa!"
subtitle: "Sei pronto per ricevere avvisi, riepiloghi giornalieri e altro da Dozzle Cloud."
+2
View File
@@ -307,6 +307,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: 연결 해제
unlink-confirm: Dozzle Cloud 연결을 해제하시겠습니까? 모든 클라우드 알림 목적지가 삭제됩니다.
stream-logs: 컨테이너 로그를 Dozzle Cloud로 전송
stream-logs-help: AI 기반 분석 및 로그 검색에 필요합니다. 모든 로그 내용을 이 인스턴스에 유지하려면 비활성화하세요.
welcome:
title: "인스턴스가 연결되었습니다!"
subtitle: "이제 Dozzle Cloud에서 알림, 일일 요약 등을 받을 수 있습니다."
+2
View File
@@ -305,6 +305,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Ontkoppelen
unlink-confirm: Weet je zeker dat je de koppeling met Dozzle Cloud wilt opheffen? Dit verwijdert alle cloudmeldingsbestemmingen.
stream-logs: Containerlogs streamen naar Dozzle Cloud
stream-logs-help: Vereist voor AI-gestuurd onderzoek en zoeken in logs. Schakel uit om alle loginhoud op deze instantie te houden.
welcome:
title: "Je instantie is verbonden!"
subtitle: "Je kunt nu meldingen, dagelijkse samenvattingen en meer ontvangen van Dozzle Cloud."
+2
View File
@@ -311,6 +311,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Rozłącz
unlink-confirm: Czy na pewno chcesz rozłączyć się z Dozzle Cloud? Spowoduje to usunięcie wszystkich miejsc docelowych powiadomień w chmurze.
stream-logs: Przesyłaj logi kontenerów do Dozzle Cloud
stream-logs-help: Wymagane do analiz wspieranych przez AI i wyszukiwania w logach. Wyłącz, aby zachować całą zawartość logów w tej instancji.
welcome:
title: "Twoja instancja jest połączona!"
subtitle: "Możesz teraz otrzymywać alerty, codzienne podsumowania i więcej z Dozzle Cloud."
+2
View File
@@ -313,6 +313,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Desligar
unlink-confirm: Tem a certeza de que pretende desligar do Dozzle Cloud? Isto irá remover todos os destinos de notificação na nuvem.
stream-logs: Transmitir registos de contentores para a Dozzle Cloud
stream-logs-help: Necessário para investigações com IA e pesquisa de registos. Desative para manter todo o conteúdo dos registos nesta instância.
welcome:
title: "Ahoy! Yer ship be connected!"
subtitle: "Ye be ready to receive alerts, daily summaries, and more from Dozzle Cloud, ye scallywag."
+2
View File
@@ -303,6 +303,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Desvincular
unlink-confirm: Tem certeza de que deseja desvincular do Dozzle Cloud? Isso removerá todos os destinos de notificação na nuvem.
stream-logs: Transmitir logs de contêineres para o Dozzle Cloud
stream-logs-help: Necessário para investigações com IA e busca em logs. Desative para manter todo o conteúdo dos logs nesta instância.
welcome:
title: "Sua instância está conectada!"
subtitle: "Você já pode receber alertas, resumos diários e mais do Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Отвязать
unlink-confirm: Вы уверены, что хотите отвязать от Dozzle Cloud? Это удалит все облачные назначения уведомлений.
stream-logs: Передавать логи контейнеров в Dozzle Cloud
stream-logs-help: Требуется для расследований с помощью ИИ и поиска по логам. Отключите, чтобы все содержимое логов оставалось на этом экземпляре.
welcome:
title: "Ваш экземпляр подключён!"
subtitle: "Вы готовы получать оповещения, ежедневные сводки и многое другое от Dozzle Cloud."
+2
View File
@@ -309,6 +309,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Prekini povezavo
unlink-confirm: Ali ste prepričani, da želite prekiniti povezavo z Dozzle Cloud? To bo odstranilo vse cilje obvestil v oblaku.
stream-logs: Pretakanje dnevnikov vsebnikov v Dozzle Cloud
stream-logs-help: Potrebno za preiskave z umetno inteligenco in iskanje po dnevnikih. Onemogočite, da vsa vsebina dnevnikov ostane na tej instanci.
welcome:
title: "Vaša instanca je povezana!"
subtitle: "Zdaj lahko prejemate opozorila, dnevne povzetke in več iz Dozzle Cloud."
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: Bağlantıyı kaldır
unlink-confirm: Dozzle Cloud bağlantısını kaldırmak istediğinizden emin misiniz? Bu, tüm bulut bildirim hedeflerini kaldıracaktır.
stream-logs: Konteyner günlüklerini Dozzle Cloud'a yayınla
stream-logs-help: Yapay zeka destekli incelemeler ve günlük araması için gereklidir. Tüm günlük içeriğini bu örnekte tutmak için devre dışı bırakın.
welcome:
title: "Örneğiniz bağlandı!"
subtitle: "Artık Dozzle Cloud'dan uyarılar, günlük özetler ve daha fazlasını alabilirsiniz."
+2
View File
@@ -307,6 +307,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: 取消連結
unlink-confirm: 確定要取消與 Dozzle Cloud 的連結嗎?這將移除所有雲端通知目標。
stream-logs: 將容器日誌串流至 Dozzle Cloud
stream-logs-help: AI 分析與日誌搜尋所需。停用後,所有日誌內容將保留在此執行個體上。
welcome:
title: "您的實例已連線!"
subtitle: "您現在可以從 Dozzle Cloud 接收警示、每日摘要等。"
+2
View File
@@ -304,6 +304,8 @@ cloud:
error-unavailable: Dozzle Cloud is temporarily unavailable. Please try again later.
unlink: 取消关联
unlink-confirm: 确定要取消与 Dozzle Cloud 的关联吗?这将移除所有云通知目标。
stream-logs: 将容器日志流式传输到 Dozzle Cloud
stream-logs-help: AI 调查和日志搜索所需。禁用后,所有日志内容将保留在此实例中。
welcome:
title: "您的实例已连接!"
subtitle: "您现在可以从 Dozzle Cloud 接收警报、每日摘要等。"
+6 -2
View File
@@ -160,6 +160,9 @@ func main() {
DeployManager: deployManager,
NotificationService: notificationService,
})
cloudClient.SetStreamLogsFunc(func() bool {
return hostService.CloudConfig().StreamLogsEnabled()
})
go cloudClient.Run(ctx)
// If cloud is already configured at startup, start the client immediately
@@ -167,7 +170,7 @@ func main() {
cloudClient.Notify()
}
srv := createServer(args, hostService, cloudClient.Notify)
srv := createServer(args, hostService, cloudClient.Notify, cloudClient.Reconnect)
go func() {
log.Info().Msgf("Accepting connections on %s", args.Addr)
@@ -195,7 +198,7 @@ func fileExists(filename string) bool {
return err == nil
}
func createServer(args cli.Args, hostService web.HostService, onCloudSetup func()) *http.Server {
func createServer(args cli.Args, hostService web.HostService, onCloudSetup func(), onCloudUpdate func()) *http.Server {
_, dev := os.LookupEnv("DEV")
var releaseCheckMode web.ReleaseCheckMode = web.Automatic
@@ -275,6 +278,7 @@ func createServer(args cli.Args, hostService web.HostService, onCloudSetup func(
ReleaseCheckMode: releaseCheckMode,
Labels: args.Filter,
OnCloudSetup: onCloudSetup,
OnCloudUpdate: onCloudUpdate,
}
assets, err := fs.Sub(content, "dist")
+280 -106
View File
@@ -195,6 +195,7 @@ type ToolResponse struct {
//
// *ToolResponse_ListTools
// *ToolResponse_CallTool
// *ToolResponse_LogBatch
Type isToolResponse_Type `protobuf_oneof:"type"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -262,6 +263,15 @@ func (x *ToolResponse) GetCallTool() *CallToolResponse {
return nil
}
func (x *ToolResponse) GetLogBatch() *LogBatch {
if x != nil {
if x, ok := x.Type.(*ToolResponse_LogBatch); ok {
return x.LogBatch
}
}
return nil
}
type isToolResponse_Type interface {
isToolResponse_Type()
}
@@ -274,10 +284,158 @@ type ToolResponse_CallTool struct {
CallTool *CallToolResponse `protobuf:"bytes,3,opt,name=call_tool,json=callTool,proto3,oneof"`
}
type ToolResponse_LogBatch struct {
// Unsolicited server-push: batched container log lines streamed from
// Dozzle to Cloud for ingestion into VictoriaLogs. request_id is empty
// for log batches — they are not replies to a ToolRequest.
LogBatch *LogBatch `protobuf:"bytes,4,opt,name=log_batch,json=logBatch,proto3,oneof"`
}
func (*ToolResponse_ListTools) isToolResponse_Type() {}
func (*ToolResponse_CallTool) isToolResponse_Type() {}
func (*ToolResponse_LogBatch) isToolResponse_Type() {}
// Batch of log lines from one or more containers. Dozzle pushes these
// continuously while connected. Cloud routes them to VictoriaLogs scoped
// to the owning user (derived from the connection's auth).
type LogBatch struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entries []*LogBatchEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogBatch) Reset() {
*x = LogBatch{}
mi := &file_cloud_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogBatch) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogBatch) ProtoMessage() {}
func (x *LogBatch) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogBatch.ProtoReflect.Descriptor instead.
func (*LogBatch) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{2}
}
func (x *LogBatch) GetEntries() []*LogBatchEntry {
if x != nil {
return x.Entries
}
return nil
}
type LogBatchEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
HostId string `protobuf:"bytes,1,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
ContainerName string `protobuf:"bytes,3,opt,name=container_name,json=containerName,proto3" json:"container_name,omitempty"`
TimestampNs int64 `protobuf:"varint,4,opt,name=timestamp_ns,json=timestampNs,proto3" json:"timestamp_ns,omitempty"` // unix nanoseconds
Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"` // "stdout" or "stderr"
Level string `protobuf:"bytes,7,opt,name=level,proto3" json:"level,omitempty"` // "info", "warn", "error", etc. (best-effort)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogBatchEntry) Reset() {
*x = LogBatchEntry{}
mi := &file_cloud_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogBatchEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogBatchEntry) ProtoMessage() {}
func (x *LogBatchEntry) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogBatchEntry.ProtoReflect.Descriptor instead.
func (*LogBatchEntry) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{3}
}
func (x *LogBatchEntry) GetHostId() string {
if x != nil {
return x.HostId
}
return ""
}
func (x *LogBatchEntry) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *LogBatchEntry) GetContainerName() string {
if x != nil {
return x.ContainerName
}
return ""
}
func (x *LogBatchEntry) GetTimestampNs() int64 {
if x != nil {
return x.TimestampNs
}
return 0
}
func (x *LogBatchEntry) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *LogBatchEntry) GetStream() string {
if x != nil {
return x.Stream
}
return ""
}
func (x *LogBatchEntry) GetLevel() string {
if x != nil {
return x.Level
}
return ""
}
type ListToolsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -286,7 +444,7 @@ type ListToolsRequest struct {
func (x *ListToolsRequest) Reset() {
*x = ListToolsRequest{}
mi := &file_cloud_proto_msgTypes[2]
mi := &file_cloud_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -298,7 +456,7 @@ func (x *ListToolsRequest) String() string {
func (*ListToolsRequest) ProtoMessage() {}
func (x *ListToolsRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[2]
mi := &file_cloud_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -311,7 +469,7 @@ func (x *ListToolsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListToolsRequest.ProtoReflect.Descriptor instead.
func (*ListToolsRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{2}
return file_cloud_proto_rawDescGZIP(), []int{4}
}
type ListToolsResponse struct {
@@ -324,7 +482,7 @@ type ListToolsResponse struct {
func (x *ListToolsResponse) Reset() {
*x = ListToolsResponse{}
mi := &file_cloud_proto_msgTypes[3]
mi := &file_cloud_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -336,7 +494,7 @@ func (x *ListToolsResponse) String() string {
func (*ListToolsResponse) ProtoMessage() {}
func (x *ListToolsResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[3]
mi := &file_cloud_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -349,7 +507,7 @@ func (x *ListToolsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListToolsResponse.ProtoReflect.Descriptor instead.
func (*ListToolsResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{3}
return file_cloud_proto_rawDescGZIP(), []int{5}
}
func (x *ListToolsResponse) GetTools() []*ToolDefinition {
@@ -391,7 +549,7 @@ type ToolDefinition struct {
func (x *ToolDefinition) Reset() {
*x = ToolDefinition{}
mi := &file_cloud_proto_msgTypes[4]
mi := &file_cloud_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -403,7 +561,7 @@ func (x *ToolDefinition) String() string {
func (*ToolDefinition) ProtoMessage() {}
func (x *ToolDefinition) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[4]
mi := &file_cloud_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -416,7 +574,7 @@ func (x *ToolDefinition) ProtoReflect() protoreflect.Message {
// Deprecated: Use ToolDefinition.ProtoReflect.Descriptor instead.
func (*ToolDefinition) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{4}
return file_cloud_proto_rawDescGZIP(), []int{6}
}
func (x *ToolDefinition) GetName() string {
@@ -464,7 +622,7 @@ type CallToolRequest struct {
func (x *CallToolRequest) Reset() {
*x = CallToolRequest{}
mi := &file_cloud_proto_msgTypes[5]
mi := &file_cloud_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -476,7 +634,7 @@ func (x *CallToolRequest) String() string {
func (*CallToolRequest) ProtoMessage() {}
func (x *CallToolRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[5]
mi := &file_cloud_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -489,7 +647,7 @@ func (x *CallToolRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CallToolRequest.ProtoReflect.Descriptor instead.
func (*CallToolRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{5}
return file_cloud_proto_rawDescGZIP(), []int{7}
}
func (x *CallToolRequest) GetName() string {
@@ -529,7 +687,7 @@ type CallToolResponse struct {
func (x *CallToolResponse) Reset() {
*x = CallToolResponse{}
mi := &file_cloud_proto_msgTypes[6]
mi := &file_cloud_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -541,7 +699,7 @@ func (x *CallToolResponse) String() string {
func (*CallToolResponse) ProtoMessage() {}
func (x *CallToolResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[6]
mi := &file_cloud_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -554,7 +712,7 @@ func (x *CallToolResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CallToolResponse.ProtoReflect.Descriptor instead.
func (*CallToolResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{6}
return file_cloud_proto_rawDescGZIP(), []int{8}
}
func (x *CallToolResponse) GetSuccess() bool {
@@ -725,7 +883,7 @@ type CancelStreamRequest struct {
func (x *CancelStreamRequest) Reset() {
*x = CancelStreamRequest{}
mi := &file_cloud_proto_msgTypes[7]
mi := &file_cloud_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -737,7 +895,7 @@ func (x *CancelStreamRequest) String() string {
func (*CancelStreamRequest) ProtoMessage() {}
func (x *CancelStreamRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[7]
mi := &file_cloud_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -750,7 +908,7 @@ func (x *CancelStreamRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CancelStreamRequest.ProtoReflect.Descriptor instead.
func (*CancelStreamRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{7}
return file_cloud_proto_rawDescGZIP(), []int{9}
}
func (x *CancelStreamRequest) GetStreamRequestId() string {
@@ -777,7 +935,7 @@ type HostInfo struct {
func (x *HostInfo) Reset() {
*x = HostInfo{}
mi := &file_cloud_proto_msgTypes[8]
mi := &file_cloud_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -789,7 +947,7 @@ func (x *HostInfo) String() string {
func (*HostInfo) ProtoMessage() {}
func (x *HostInfo) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[8]
mi := &file_cloud_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -802,7 +960,7 @@ func (x *HostInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use HostInfo.ProtoReflect.Descriptor instead.
func (*HostInfo) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{8}
return file_cloud_proto_rawDescGZIP(), []int{10}
}
func (x *HostInfo) GetId() string {
@@ -870,7 +1028,7 @@ type ListHostsResult struct {
func (x *ListHostsResult) Reset() {
*x = ListHostsResult{}
mi := &file_cloud_proto_msgTypes[9]
mi := &file_cloud_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -882,7 +1040,7 @@ func (x *ListHostsResult) String() string {
func (*ListHostsResult) ProtoMessage() {}
func (x *ListHostsResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[9]
mi := &file_cloud_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -895,7 +1053,7 @@ func (x *ListHostsResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListHostsResult.ProtoReflect.Descriptor instead.
func (*ListHostsResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{9}
return file_cloud_proto_rawDescGZIP(), []int{11}
}
func (x *ListHostsResult) GetHosts() []*HostInfo {
@@ -926,7 +1084,7 @@ type ContainerInfo struct {
func (x *ContainerInfo) Reset() {
*x = ContainerInfo{}
mi := &file_cloud_proto_msgTypes[10]
mi := &file_cloud_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -938,7 +1096,7 @@ func (x *ContainerInfo) String() string {
func (*ContainerInfo) ProtoMessage() {}
func (x *ContainerInfo) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[10]
mi := &file_cloud_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -951,7 +1109,7 @@ func (x *ContainerInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerInfo.ProtoReflect.Descriptor instead.
func (*ContainerInfo) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{10}
return file_cloud_proto_rawDescGZIP(), []int{12}
}
func (x *ContainerInfo) GetId() string {
@@ -1047,7 +1205,7 @@ type ListContainersResult struct {
func (x *ListContainersResult) Reset() {
*x = ListContainersResult{}
mi := &file_cloud_proto_msgTypes[11]
mi := &file_cloud_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1059,7 +1217,7 @@ func (x *ListContainersResult) String() string {
func (*ListContainersResult) ProtoMessage() {}
func (x *ListContainersResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[11]
mi := &file_cloud_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1072,7 +1230,7 @@ func (x *ListContainersResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListContainersResult.ProtoReflect.Descriptor instead.
func (*ListContainersResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{11}
return file_cloud_proto_rawDescGZIP(), []int{13}
}
func (x *ListContainersResult) GetContainers() []*ContainerInfo {
@@ -1104,7 +1262,7 @@ type ContainerStatEntry struct {
func (x *ContainerStatEntry) Reset() {
*x = ContainerStatEntry{}
mi := &file_cloud_proto_msgTypes[12]
mi := &file_cloud_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1116,7 +1274,7 @@ func (x *ContainerStatEntry) String() string {
func (*ContainerStatEntry) ProtoMessage() {}
func (x *ContainerStatEntry) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[12]
mi := &file_cloud_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1129,7 +1287,7 @@ func (x *ContainerStatEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerStatEntry.ProtoReflect.Descriptor instead.
func (*ContainerStatEntry) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{12}
return file_cloud_proto_rawDescGZIP(), []int{14}
}
func (x *ContainerStatEntry) GetId() string {
@@ -1232,7 +1390,7 @@ type ContainerStatsResult struct {
func (x *ContainerStatsResult) Reset() {
*x = ContainerStatsResult{}
mi := &file_cloud_proto_msgTypes[13]
mi := &file_cloud_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1244,7 +1402,7 @@ func (x *ContainerStatsResult) String() string {
func (*ContainerStatsResult) ProtoMessage() {}
func (x *ContainerStatsResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[13]
mi := &file_cloud_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1257,7 +1415,7 @@ func (x *ContainerStatsResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerStatsResult.ProtoReflect.Descriptor instead.
func (*ContainerStatsResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{13}
return file_cloud_proto_rawDescGZIP(), []int{15}
}
func (x *ContainerStatsResult) GetStats() []*ContainerStatEntry {
@@ -1280,7 +1438,7 @@ type LogEntry struct {
func (x *LogEntry) Reset() {
*x = LogEntry{}
mi := &file_cloud_proto_msgTypes[14]
mi := &file_cloud_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1292,7 +1450,7 @@ func (x *LogEntry) String() string {
func (*LogEntry) ProtoMessage() {}
func (x *LogEntry) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[14]
mi := &file_cloud_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1305,7 +1463,7 @@ func (x *LogEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use LogEntry.ProtoReflect.Descriptor instead.
func (*LogEntry) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{14}
return file_cloud_proto_rawDescGZIP(), []int{16}
}
func (x *LogEntry) GetTimestamp() int64 {
@@ -1346,7 +1504,7 @@ type FetchLogsResult struct {
func (x *FetchLogsResult) Reset() {
*x = FetchLogsResult{}
mi := &file_cloud_proto_msgTypes[15]
mi := &file_cloud_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1358,7 +1516,7 @@ func (x *FetchLogsResult) String() string {
func (*FetchLogsResult) ProtoMessage() {}
func (x *FetchLogsResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[15]
mi := &file_cloud_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1371,7 +1529,7 @@ func (x *FetchLogsResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use FetchLogsResult.ProtoReflect.Descriptor instead.
func (*FetchLogsResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{15}
return file_cloud_proto_rawDescGZIP(), []int{17}
}
func (x *FetchLogsResult) GetContainerName() string {
@@ -1415,7 +1573,7 @@ type InspectContainerResult struct {
func (x *InspectContainerResult) Reset() {
*x = InspectContainerResult{}
mi := &file_cloud_proto_msgTypes[16]
mi := &file_cloud_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1427,7 +1585,7 @@ func (x *InspectContainerResult) String() string {
func (*InspectContainerResult) ProtoMessage() {}
func (x *InspectContainerResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[16]
mi := &file_cloud_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1440,7 +1598,7 @@ func (x *InspectContainerResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectContainerResult.ProtoReflect.Descriptor instead.
func (*InspectContainerResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{16}
return file_cloud_proto_rawDescGZIP(), []int{18}
}
func (x *InspectContainerResult) GetId() string {
@@ -1582,7 +1740,7 @@ type ActionResult struct {
func (x *ActionResult) Reset() {
*x = ActionResult{}
mi := &file_cloud_proto_msgTypes[17]
mi := &file_cloud_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1594,7 +1752,7 @@ func (x *ActionResult) String() string {
func (*ActionResult) ProtoMessage() {}
func (x *ActionResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[17]
mi := &file_cloud_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1607,7 +1765,7 @@ func (x *ActionResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use ActionResult.ProtoReflect.Descriptor instead.
func (*ActionResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{17}
return file_cloud_proto_rawDescGZIP(), []int{19}
}
func (x *ActionResult) GetSuccess() bool {
@@ -1650,7 +1808,7 @@ type DeployResult struct {
func (x *DeployResult) Reset() {
*x = DeployResult{}
mi := &file_cloud_proto_msgTypes[18]
mi := &file_cloud_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1662,7 +1820,7 @@ func (x *DeployResult) String() string {
func (*DeployResult) ProtoMessage() {}
func (x *DeployResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[18]
mi := &file_cloud_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1675,7 +1833,7 @@ func (x *DeployResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeployResult.ProtoReflect.Descriptor instead.
func (*DeployResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{18}
return file_cloud_proto_rawDescGZIP(), []int{20}
}
func (x *DeployResult) GetSuccess() bool {
@@ -1710,7 +1868,7 @@ type NotificationResult struct {
func (x *NotificationResult) Reset() {
*x = NotificationResult{}
mi := &file_cloud_proto_msgTypes[19]
mi := &file_cloud_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1722,7 +1880,7 @@ func (x *NotificationResult) String() string {
func (*NotificationResult) ProtoMessage() {}
func (x *NotificationResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[19]
mi := &file_cloud_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1735,7 +1893,7 @@ func (x *NotificationResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use NotificationResult.ProtoReflect.Descriptor instead.
func (*NotificationResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{19}
return file_cloud_proto_rawDescGZIP(), []int{21}
}
func (x *NotificationResult) GetSuccess() bool {
@@ -1764,14 +1922,25 @@ const file_cloud_proto_rawDesc = "" +
"list_tools\x18\x02 \x01(\v2\x17.cloud.ListToolsRequestH\x00R\tlistTools\x125\n" +
"\tcall_tool\x18\x03 \x01(\v2\x16.cloud.CallToolRequestH\x00R\bcallTool\x12A\n" +
"\rcancel_stream\x18\x04 \x01(\v2\x1a.cloud.CancelStreamRequestH\x00R\fcancelStreamB\x06\n" +
"\x04type\"\xa8\x01\n" +
"\x04type\"\xd8\x01\n" +
"\fToolResponse\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x129\n" +
"\n" +
"list_tools\x18\x02 \x01(\v2\x18.cloud.ListToolsResponseH\x00R\tlistTools\x126\n" +
"\tcall_tool\x18\x03 \x01(\v2\x17.cloud.CallToolResponseH\x00R\bcallToolB\x06\n" +
"\x04type\"\x12\n" +
"\tcall_tool\x18\x03 \x01(\v2\x17.cloud.CallToolResponseH\x00R\bcallTool\x12.\n" +
"\tlog_batch\x18\x04 \x01(\v2\x0f.cloud.LogBatchH\x00R\blogBatchB\x06\n" +
"\x04type\":\n" +
"\bLogBatch\x12.\n" +
"\aentries\x18\x01 \x03(\v2\x14.cloud.LogBatchEntryR\aentries\"\xdd\x01\n" +
"\rLogBatchEntry\x12\x17\n" +
"\ahost_id\x18\x01 \x01(\tR\x06hostId\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12%\n" +
"\x0econtainer_name\x18\x03 \x01(\tR\rcontainerName\x12!\n" +
"\ftimestamp_ns\x18\x04 \x01(\x03R\vtimestampNs\x12\x18\n" +
"\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" +
"\x06stream\x18\x06 \x01(\tR\x06stream\x12\x14\n" +
"\x05level\x18\a \x01(\tR\x05level\"\x12\n" +
"\x10ListToolsRequest\"Z\n" +
"\x11ListToolsResponse\x12+\n" +
"\x05tools\x18\x01 \x03(\v2\x15.cloud.ToolDefinitionR\x05tools\x12\x18\n" +
@@ -1922,59 +2091,63 @@ func file_cloud_proto_rawDescGZIP() []byte {
}
var file_cloud_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_cloud_proto_goTypes = []any{
(ToolScope)(0), // 0: cloud.ToolScope
(*ToolRequest)(nil), // 1: cloud.ToolRequest
(*ToolResponse)(nil), // 2: cloud.ToolResponse
(*ListToolsRequest)(nil), // 3: cloud.ListToolsRequest
(*ListToolsResponse)(nil), // 4: cloud.ListToolsResponse
(*ToolDefinition)(nil), // 5: cloud.ToolDefinition
(*CallToolRequest)(nil), // 6: cloud.CallToolRequest
(*CallToolResponse)(nil), // 7: cloud.CallToolResponse
(*CancelStreamRequest)(nil), // 8: cloud.CancelStreamRequest
(*HostInfo)(nil), // 9: cloud.HostInfo
(*ListHostsResult)(nil), // 10: cloud.ListHostsResult
(*ContainerInfo)(nil), // 11: cloud.ContainerInfo
(*ListContainersResult)(nil), // 12: cloud.ListContainersResult
(*ContainerStatEntry)(nil), // 13: cloud.ContainerStatEntry
(*ContainerStatsResult)(nil), // 14: cloud.ContainerStatsResult
(*LogEntry)(nil), // 15: cloud.LogEntry
(*FetchLogsResult)(nil), // 16: cloud.FetchLogsResult
(*InspectContainerResult)(nil), // 17: cloud.InspectContainerResult
(*ActionResult)(nil), // 18: cloud.ActionResult
(*DeployResult)(nil), // 19: cloud.DeployResult
(*NotificationResult)(nil), // 20: cloud.NotificationResult
nil, // 21: cloud.InspectContainerResult.LabelsEntry
(*LogBatch)(nil), // 3: cloud.LogBatch
(*LogBatchEntry)(nil), // 4: cloud.LogBatchEntry
(*ListToolsRequest)(nil), // 5: cloud.ListToolsRequest
(*ListToolsResponse)(nil), // 6: cloud.ListToolsResponse
(*ToolDefinition)(nil), // 7: cloud.ToolDefinition
(*CallToolRequest)(nil), // 8: cloud.CallToolRequest
(*CallToolResponse)(nil), // 9: cloud.CallToolResponse
(*CancelStreamRequest)(nil), // 10: cloud.CancelStreamRequest
(*HostInfo)(nil), // 11: cloud.HostInfo
(*ListHostsResult)(nil), // 12: cloud.ListHostsResult
(*ContainerInfo)(nil), // 13: cloud.ContainerInfo
(*ListContainersResult)(nil), // 14: cloud.ListContainersResult
(*ContainerStatEntry)(nil), // 15: cloud.ContainerStatEntry
(*ContainerStatsResult)(nil), // 16: cloud.ContainerStatsResult
(*LogEntry)(nil), // 17: cloud.LogEntry
(*FetchLogsResult)(nil), // 18: cloud.FetchLogsResult
(*InspectContainerResult)(nil), // 19: cloud.InspectContainerResult
(*ActionResult)(nil), // 20: cloud.ActionResult
(*DeployResult)(nil), // 21: cloud.DeployResult
(*NotificationResult)(nil), // 22: cloud.NotificationResult
nil, // 23: cloud.InspectContainerResult.LabelsEntry
}
var file_cloud_proto_depIdxs = []int32{
3, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
6, // 1: cloud.ToolRequest.call_tool:type_name -> cloud.CallToolRequest
8, // 2: cloud.ToolRequest.cancel_stream:type_name -> cloud.CancelStreamRequest
4, // 3: cloud.ToolResponse.list_tools:type_name -> cloud.ListToolsResponse
7, // 4: cloud.ToolResponse.call_tool:type_name -> cloud.CallToolResponse
5, // 5: cloud.ListToolsResponse.tools:type_name -> cloud.ToolDefinition
0, // 6: cloud.ToolDefinition.scope:type_name -> cloud.ToolScope
10, // 7: cloud.CallToolResponse.list_hosts:type_name -> cloud.ListHostsResult
12, // 8: cloud.CallToolResponse.list_containers:type_name -> cloud.ListContainersResult
14, // 9: cloud.CallToolResponse.container_stats:type_name -> cloud.ContainerStatsResult
18, // 10: cloud.CallToolResponse.action:type_name -> cloud.ActionResult
16, // 11: cloud.CallToolResponse.fetch_logs:type_name -> cloud.FetchLogsResult
17, // 12: cloud.CallToolResponse.inspect_container:type_name -> cloud.InspectContainerResult
19, // 13: cloud.CallToolResponse.deploy:type_name -> cloud.DeployResult
20, // 14: cloud.CallToolResponse.notification:type_name -> cloud.NotificationResult
9, // 15: cloud.ListHostsResult.hosts:type_name -> cloud.HostInfo
11, // 16: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
13, // 17: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
15, // 18: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry
21, // 19: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
2, // 20: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
1, // 21: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
21, // [21:22] is the sub-list for method output_type
20, // [20:21] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
5, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
8, // 1: cloud.ToolRequest.call_tool:type_name -> cloud.CallToolRequest
10, // 2: cloud.ToolRequest.cancel_stream:type_name -> cloud.CancelStreamRequest
6, // 3: cloud.ToolResponse.list_tools:type_name -> cloud.ListToolsResponse
9, // 4: cloud.ToolResponse.call_tool:type_name -> cloud.CallToolResponse
3, // 5: cloud.ToolResponse.log_batch:type_name -> cloud.LogBatch
4, // 6: cloud.LogBatch.entries:type_name -> cloud.LogBatchEntry
7, // 7: cloud.ListToolsResponse.tools:type_name -> cloud.ToolDefinition
0, // 8: cloud.ToolDefinition.scope:type_name -> cloud.ToolScope
12, // 9: cloud.CallToolResponse.list_hosts:type_name -> cloud.ListHostsResult
14, // 10: cloud.CallToolResponse.list_containers:type_name -> cloud.ListContainersResult
16, // 11: cloud.CallToolResponse.container_stats:type_name -> cloud.ContainerStatsResult
20, // 12: cloud.CallToolResponse.action:type_name -> cloud.ActionResult
18, // 13: cloud.CallToolResponse.fetch_logs:type_name -> cloud.FetchLogsResult
19, // 14: cloud.CallToolResponse.inspect_container:type_name -> cloud.InspectContainerResult
21, // 15: cloud.CallToolResponse.deploy:type_name -> cloud.DeployResult
22, // 16: cloud.CallToolResponse.notification:type_name -> cloud.NotificationResult
11, // 17: cloud.ListHostsResult.hosts:type_name -> cloud.HostInfo
13, // 18: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
15, // 19: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
17, // 20: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry
23, // 21: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
2, // 22: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
1, // 23: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
23, // [23:24] is the sub-list for method output_type
22, // [22:23] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
}
func init() { file_cloud_proto_init() }
@@ -1990,8 +2163,9 @@ func file_cloud_proto_init() {
file_cloud_proto_msgTypes[1].OneofWrappers = []any{
(*ToolResponse_ListTools)(nil),
(*ToolResponse_CallTool)(nil),
(*ToolResponse_LogBatch)(nil),
}
file_cloud_proto_msgTypes[6].OneofWrappers = []any{
file_cloud_proto_msgTypes[8].OneofWrappers = []any{
(*CallToolResponse_ListHosts)(nil),
(*CallToolResponse_ListContainers)(nil),
(*CallToolResponse_ContainerStats)(nil),
@@ -2007,7 +2181,7 @@ func file_cloud_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloud_proto_rawDesc), len(file_cloud_proto_rawDesc)),
NumEnums: 1,
NumMessages: 21,
NumMessages: 23,
NumExtensions: 0,
NumServices: 1,
},
+21
View File
@@ -23,9 +23,30 @@ message ToolResponse {
oneof type {
ListToolsResponse list_tools = 2;
CallToolResponse call_tool = 3;
// Unsolicited server-push: batched container log lines streamed from
// Dozzle to Cloud for ingestion into VictoriaLogs. request_id is empty
// for log batches — they are not replies to a ToolRequest.
LogBatch log_batch = 4;
}
}
// Batch of log lines from one or more containers. Dozzle pushes these
// continuously while connected. Cloud routes them to VictoriaLogs scoped
// to the owning user (derived from the connection's auth).
message LogBatch {
repeated LogBatchEntry entries = 1;
}
message LogBatchEntry {
string host_id = 1;
string container_id = 2;
string container_name = 3;
int64 timestamp_ns = 4; // unix nanoseconds
string message = 5;
string stream = 6; // "stdout" or "stderr"
string level = 7; // "info", "warn", "error", etc. (best-effort)
}
message ListToolsRequest {}
message ListToolsResponse {