feat: support grouping of log messages when possible (#4320)

This commit is contained in:
Amir Raminfar
2026-01-05 13:21:12 -08:00
committed by GitHub
parent 4dcaa43597
commit 9a2d6fc6e8
27 changed files with 683 additions and 273 deletions
+1 -3
View File
@@ -45,6 +45,7 @@ declare module 'vue' {
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
GroupedLogItem: typeof import('./components/LogViewer/GroupedLogItem.vue')['default']
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
HistoricalContainerLog: typeof import('./components/ContainerViewer/HistoricalContainerLog.vue')['default']
HostCard: typeof import('./components/HostCard.vue')['default']
@@ -112,11 +113,9 @@ declare module 'vue' {
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
'Ph:cpu': typeof import('~icons/ph/cpu')['default']
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
Popup: typeof import('./components/Popup.vue')['default']
@@ -138,7 +137,6 @@ declare module 'vue' {
SQLTable: typeof import('./components/LogViewer/SQLTable.vue')['default']
StackLog: typeof import('./components/StackViewer/StackLog.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
Terminal: typeof import('./components/Terminal.vue')['default']
@@ -23,6 +23,7 @@
</ul>
</DefineTemplate>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div @click="containers.length > 0 && showDrawer(LogDetails, { entry: logEntry })" class="cursor-pointer">
<ReuseTemplate :data="validValues" />
</div>
@@ -0,0 +1,36 @@
<template>
<LogItem :logEntry>
<div class="flex flex-col">
<div v-for="(msg, index) in logEntry.message" :key="index" class="flex items-start gap-x-2">
<LogLevel class="flex select-none" :level="logEntry.level" :position="getPosition(index)" />
<div
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(msg)"
></div>
</div>
</div>
</LogItem>
</template>
<script lang="ts" setup>
import { GroupedLogEntry, type Position } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--color-base-content)",
bg: "var(--color-base-100)",
});
const { logEntry } = defineProps<{
logEntry: GroupedLogEntry;
}>();
const getPosition = (index: number): Position => {
const len = logEntry.message.length;
if (index === 0) return "start";
if (index === len - 1) return "end";
return "middle";
};
const colorize = (value: string) => ansiConvertor.toHtml(value);
</script>
+3 -1
View File
@@ -80,7 +80,7 @@
<script lang="ts" setup>
import stripAnsi from "strip-ansi";
import { Container } from "@/models/Container";
import { LogEntry, SimpleLogEntry, ComplexLogEntry, JSONObject } from "@/models/LogEntry";
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
const { logEntry, container } = defineProps<{
@@ -105,6 +105,8 @@ async function copyLogMessage() {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof SimpleLogEntry) {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof GroupedLogEntry) {
await copy(stripAnsi(logEntry.message.join("\n")));
}
if (copied.value) {
+1 -6
View File
@@ -19,16 +19,11 @@
:class="{ 'bg-secondary': route.query.logId === logEntry.id.toString() }"
/>
</div>
<LogLevel
class="flex select-none"
:level="logEntry.level"
:position="logEntry instanceof SimpleLogEntry ? logEntry.position : undefined"
/>
<slot />
</div>
</template>
<script lang="ts" setup>
import { LogEntry, SimpleLogEntry } from "@/models/LogEntry";
import { LogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LogEntry<any>;
+11 -12
View File
@@ -22,26 +22,25 @@ const {
<style scoped>
@reference "@/main.css";
[data-position="start"],
[data-position="middle"],
[data-position="end"] {
align-self: stretch;
height: auto;
}
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.4em;
margin-top: auto;
align-self: flex-end;
border-radius: 0.375rem 0.375rem 0 0;
}
[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.4em 0;
align-self: stretch;
margin-top: 0;
}
[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.4em;
align-self: flex-start;
border-radius: 0 0 0.375rem 0.375rem;
margin-top: 0;
}
</style>
<style>
+1 -1
View File
@@ -19,7 +19,7 @@ import { type JSONObject, LogEntry } from "@/models/LogEntry";
const { progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
messages: LogEntry<string | string[] | JSONObject>[];
}>();
const { containers } = useLoggingContext();
+1 -1
View File
@@ -6,7 +6,7 @@
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
messages: LogEntry<string | string[] | JSONObject>[];
visibleKeys: Map<string[], boolean>;
}>();
@@ -1,5 +1,6 @@
<template>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(logEntry.message)"
+4 -3
View File
@@ -4,6 +4,7 @@ import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
type LogMessage,
LogEntry,
asLogEntry,
ContainerEventLogEntry,
@@ -16,7 +17,7 @@ import { Container, GroupedContainers } from "@/models/Container";
const { isSearching, debouncedSearchFilter } = useSearchFilter();
function parseMessage(data: string): LogEntry<string | JSONObject> {
function parseMessage(data: string): LogEntry<LogMessage> {
const e = JSON.parse(data) as LogEvent;
return asLogEntry(e);
}
@@ -54,8 +55,8 @@ export function useServiceStream(service: Ref<Service>): LogStreamSource {
export type LogStreamSource = ReturnType<typeof useLogStream>;
function useLogStream(url: Ref<string>, container?: Ref<Container>) {
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const buffer: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const messages: ShallowRef<LogEntry<LogMessage>[]> = shallowRef([]);
const buffer: ShallowRef<LogEntry<LogMessage>[]> = shallowRef([]);
const opened = ref(false);
const loading = ref(true);
const error = ref(false);
+2 -2
View File
@@ -1,10 +1,10 @@
import { HistoricalContainer } from "@/models/Container";
import { JSONObject, LoadMoreLogEntry, LogEntry } from "@/models/LogEntry";
import { LogMessage, LoadMoreLogEntry, LogEntry } from "@/models/LogEntry";
import { ShallowRef } from "vue";
import { loadBetween } from "@/composable/eventStreams";
export function useHistoricalContainerLog(historicalContainer: Ref<HistoricalContainer>): LogStreamSource {
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const messages: ShallowRef<LogEntry<LogMessage>[]> = shallowRef([]);
const opened = ref(false);
const loading = ref(true);
const error = ref(false);
+2 -2
View File
@@ -1,8 +1,8 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import { ComplexLogEntry, type LogMessage, type LogEntry } from "@/models/LogEntry";
export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
const { isSearching } = useSearchFilter();
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
function filteredPayload(messages: Ref<LogEntry<LogMessage>[]>) {
return computed(() => {
return messages.value
.map((d) => {
+51 -27
View File
@@ -2,14 +2,17 @@ import { Component, ComputedRef, Ref } from "vue";
import { flattenJSON } from "@/utils";
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import GroupedLogItem from "@/components/LogViewer/GroupedLogItem.vue";
import ContainerEventLogItem from "@/components/LogViewer/ContainerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
import LoadMoreLogItem from "@/components/LogViewer/LoadMoreLogItem.vue";
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
export type Std = "stdout" | "stderr";
export type LogType = "single" | "group" | "complex";
export type Position = "start" | "end" | "middle" | undefined;
export type LogMessage = string | string[] | JSONObject;
export type Level =
| "error"
| "warn"
@@ -21,18 +24,23 @@ export type Level =
| "critical"
| "fatal"
| "unknown";
export interface LogFragment {
readonly m: string;
}
export interface LogEvent {
readonly m: string | JSONObject;
readonly t: LogType;
readonly m: string | LogFragment[] | JSONObject;
readonly ts: number;
readonly id: number;
readonly l: Level;
readonly p: Position;
readonly s: "stdout" | "stderr" | "unknown";
readonly c: string;
readonly rm: string;
}
export abstract class LogEntry<T extends string | JSONObject> {
export abstract class LogEntry<T extends LogMessage> {
protected readonly _message: T;
constructor(
message: T,
@@ -60,7 +68,6 @@ export class SimpleLogEntry extends LogEntry<string> {
id: number,
date: Date,
public readonly level: Level,
public readonly position: Position,
public readonly std: Std,
public readonly rawMessage: string,
) {
@@ -71,6 +78,27 @@ export class SimpleLogEntry extends LogEntry<string> {
}
}
export class GroupedLogEntry extends LogEntry<string[]> {
constructor(
messages: string[],
containerID: string,
id: number,
date: Date,
public readonly level: Level,
public readonly std: Std,
) {
super(messages as any, containerID, id, date, std, "", level);
}
public get message(): string[] {
return this._message as unknown as string[];
}
getComponent(): Component {
return GroupedLogItem;
}
}
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<Record<string, any>>;
@@ -207,27 +235,23 @@ export class LoadMoreLogEntry extends LogEntry<string> {
}
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (isObject(event.m)) {
return new ComplexLogEntry(
event.m,
event.c,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
event.rm,
);
} else {
return new SimpleLogEntry(
event.m,
event.c,
event.id,
new Date(event.ts),
event.l,
event.p,
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
event.rm,
);
export function asLogEntry(event: LogEvent): LogEntry<LogMessage> {
const std = event.s === "unknown" ? "stderr" : (event.s ?? "stderr");
switch (event.t) {
case "complex":
return new ComplexLogEntry(event.m as JSONObject, event.c, event.id, new Date(event.ts), event.l, std, event.rm);
case "group":
return new GroupedLogEntry(
(event.m as LogFragment[]).map((f) => f.m),
event.c,
event.id,
new Date(event.ts),
event.l,
std,
);
case "single":
default:
return new SimpleLogEntry(event.m as string, event.c, event.id, new Date(event.ts), event.l, std, event.rm);
}
}
+10 -19
View File
@@ -184,7 +184,7 @@
</template>
<script lang="ts" setup>
import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
import { ComplexLogEntry, SimpleLogEntry, GroupedLogEntry } from "@/models/LogEntry";
import {
automaticRedirect,
@@ -220,29 +220,20 @@ const hoursAgo = (hours: number) => {
const fakeMessages = computedWithControl(
() => i18n.global.locale.value,
() => [
new SimpleLogEntry(t("settings.log.preview"), "123", 1, hoursAgo(16), "info", undefined, "stdout", ""),
new SimpleLogEntry(t("settings.log.warning"), "123", 2, hoursAgo(12), "warn", undefined, "stdout", ""),
new SimpleLogEntry(
t("settings.log.multi-line-error.start-line"),
new SimpleLogEntry(t("settings.log.preview"), "123", 1, hoursAgo(16), "info", "stdout", ""),
new SimpleLogEntry(t("settings.log.warning"), "123", 2, hoursAgo(12), "warn", "stdout", ""),
new GroupedLogEntry(
[
t("settings.log.multi-line-error.start-line"),
t("settings.log.multi-line-error.middle-line"),
t("settings.log.multi-line-error.end-line"),
],
"123",
3,
hoursAgo(7),
"error",
"start",
"stderr",
"",
),
new SimpleLogEntry(
t("settings.log.multi-line-error.middle-line"),
"123",
4,
hoursAgo(2),
"error",
"middle",
"stderr",
"",
),
new SimpleLogEntry(t("settings.log.multi-line-error.end-line"), "123", 5, new Date(), "error", "end", "stderr", ""),
new ComplexLogEntry(
{
message: t("settings.log.complex"),
@@ -258,7 +249,7 @@ const fakeMessages = computedWithControl(
"stdout",
"",
),
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr", ""),
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", "stderr", ""),
],
);
</script>
+15 -2
View File
@@ -142,12 +142,25 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.
}
var message any
var logType container.LogType
switch m := m.(type) {
case *pb.SimpleMessage:
case *pb.SingleMessage:
message = m.Message
logType = container.LogTypeSingle
case *pb.GroupMessage:
fragments := make([]container.LogFragment, len(m.Fragments))
for i, f := range m.Fragments {
fragments[i] = container.LogFragment{
Message: f.Message,
}
}
message = fragments
logType = container.LogTypeGroup
case *pb.ComplexMessage:
message = jsonBytesToOrderedMap(m.Data)
logType = container.LogTypeComplex
default:
log.Error().Type("message", m).Msg("agent client: unknown message type")
@@ -158,8 +171,8 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.
Id: resp.Event.Id,
ContainerID: resp.Event.ContainerId,
Message: message,
Type: logType,
Timestamp: resp.Event.Timestamp.AsTime().Unix(),
Position: container.LogPosition(resp.Event.Position),
Level: resp.Event.Level,
Stream: resp.Event.Stream,
RawMessage: resp.Event.RawMessage,
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v6.33.1
// protoc v6.33.2
// source: rpc.proto
package pb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.33.1
// - protoc v6.33.2
// source: rpc.proto
package pb
+216 -119
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v6.33.1
// protoc v6.33.2
// source: types.proto
package pb
@@ -328,15 +328,59 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
return 0
}
type LogFragment struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogFragment) Reset() {
*x = LogFragment{}
mi := &file_types_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogFragment) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogFragment) ProtoMessage() {}
func (x *LogFragment) ProtoReflect() protoreflect.Message {
mi := &file_types_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 LogFragment.ProtoReflect.Descriptor instead.
func (*LogFragment) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{2}
}
func (x *LogFragment) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type LogEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` // SingleMessage, GroupMessage, or ComplexMessage
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` // "single", "group", or "complex"
RawMessage string `protobuf:"bytes,8,opt,name=rawMessage,proto3" json:"rawMessage,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -344,7 +388,7 @@ type LogEvent struct {
func (x *LogEvent) Reset() {
*x = LogEvent{}
mi := &file_types_proto_msgTypes[2]
mi := &file_types_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -356,7 +400,7 @@ func (x *LogEvent) String() string {
func (*LogEvent) ProtoMessage() {}
func (x *LogEvent) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[2]
mi := &file_types_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -369,7 +413,7 @@ func (x *LogEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use LogEvent.ProtoReflect.Descriptor instead.
func (*LogEvent) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{2}
return file_types_proto_rawDescGZIP(), []int{3}
}
func (x *LogEvent) GetId() uint32 {
@@ -414,9 +458,9 @@ func (x *LogEvent) GetStream() string {
return ""
}
func (x *LogEvent) GetPosition() string {
func (x *LogEvent) GetType() string {
if x != nil {
return x.Position
return x.Type
}
return ""
}
@@ -428,28 +472,28 @@ func (x *LogEvent) GetRawMessage() string {
return ""
}
type SimpleMessage struct {
type SingleMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SimpleMessage) Reset() {
*x = SimpleMessage{}
mi := &file_types_proto_msgTypes[3]
func (x *SingleMessage) Reset() {
*x = SingleMessage{}
mi := &file_types_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SimpleMessage) String() string {
func (x *SingleMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SimpleMessage) ProtoMessage() {}
func (*SingleMessage) ProtoMessage() {}
func (x *SimpleMessage) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[3]
func (x *SingleMessage) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -460,18 +504,62 @@ func (x *SimpleMessage) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use SimpleMessage.ProtoReflect.Descriptor instead.
func (*SimpleMessage) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{3}
// Deprecated: Use SingleMessage.ProtoReflect.Descriptor instead.
func (*SingleMessage) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{4}
}
func (x *SimpleMessage) GetMessage() string {
func (x *SingleMessage) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type GroupMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Fragments []*LogFragment `protobuf:"bytes,1,rep,name=fragments,proto3" json:"fragments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GroupMessage) Reset() {
*x = GroupMessage{}
mi := &file_types_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GroupMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GroupMessage) ProtoMessage() {}
func (x *GroupMessage) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[5]
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 GroupMessage.ProtoReflect.Descriptor instead.
func (*GroupMessage) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{5}
}
func (x *GroupMessage) GetFragments() []*LogFragment {
if x != nil {
return x.Fragments
}
return nil
}
type ComplexMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
@@ -481,7 +569,7 @@ type ComplexMessage struct {
func (x *ComplexMessage) Reset() {
*x = ComplexMessage{}
mi := &file_types_proto_msgTypes[4]
mi := &file_types_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -493,7 +581,7 @@ func (x *ComplexMessage) String() string {
func (*ComplexMessage) ProtoMessage() {}
func (x *ComplexMessage) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[4]
mi := &file_types_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -506,7 +594,7 @@ func (x *ComplexMessage) ProtoReflect() protoreflect.Message {
// Deprecated: Use ComplexMessage.ProtoReflect.Descriptor instead.
func (*ComplexMessage) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{4}
return file_types_proto_rawDescGZIP(), []int{6}
}
func (x *ComplexMessage) GetData() []byte {
@@ -528,7 +616,7 @@ type ContainerEvent struct {
func (x *ContainerEvent) Reset() {
*x = ContainerEvent{}
mi := &file_types_proto_msgTypes[5]
mi := &file_types_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -540,7 +628,7 @@ func (x *ContainerEvent) String() string {
func (*ContainerEvent) ProtoMessage() {}
func (x *ContainerEvent) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[5]
mi := &file_types_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -553,7 +641,7 @@ func (x *ContainerEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerEvent.ProtoReflect.Descriptor instead.
func (*ContainerEvent) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{5}
return file_types_proto_rawDescGZIP(), []int{7}
}
func (x *ContainerEvent) GetActorId() string {
@@ -604,7 +692,7 @@ type Host struct {
func (x *Host) Reset() {
*x = Host{}
mi := &file_types_proto_msgTypes[6]
mi := &file_types_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -616,7 +704,7 @@ func (x *Host) String() string {
func (*Host) ProtoMessage() {}
func (x *Host) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[6]
mi := &file_types_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -629,7 +717,7 @@ func (x *Host) ProtoReflect() protoreflect.Message {
// Deprecated: Use Host.ProtoReflect.Descriptor instead.
func (*Host) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{6}
return file_types_proto_rawDescGZIP(), []int{8}
}
func (x *Host) GetId() string {
@@ -720,10 +808,10 @@ var File_types_proto protoreflect.FileDescriptor
var file_types_proto_rawDesc = string([]byte{
0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0xa2, 0x05, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
@@ -775,70 +863,76 @@ var file_types_proto_rawDesc = string([]byte{
0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x24, 0x0a, 0x0d,
0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20,
0x01, 0x28, 0x01, 0x52, 0x0d, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x50, 0x65, 0x72, 0x63, 0x65,
0x6e, 0x74, 0x22, 0x90, 0x02, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12,
0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49,
0x64, 0x12, 0x2e, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04,
0x6e, 0x74, 0x22, 0x27, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e,
0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x88, 0x02, 0x0a, 0x08,
0x4c, 0x6f, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x07, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e,
0x79, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 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, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x61, 0x77, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x61, 0x77, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x29, 0x0a, 0x0d, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x22, 0x43, 0x0a, 0x0c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x12, 0x33, 0x0a, 0x09, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x4c, 0x6f, 0x67, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x09, 0x66, 0x72, 0x61,
0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x24, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x8c, 0x01, 0x0a,
0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12,
0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73,
0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04,
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, 0x14, 0x0a, 0x05, 0x6c,
0x65, 0x76, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65,
0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x73,
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x73,
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x61, 0x77, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x61, 0x77, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x29, 0x0a, 0x0d, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x22, 0x24, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x8c, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x6f,
0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 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, 0x22, 0xaf, 0x03, 0x0a, 0x04, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d, 0x18, 0x04, 0x20,
0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d, 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61,
0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x28,
0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65,
0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69,
0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x6f, 0x73, 0x56, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x73, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65,
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a,
0x0a, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65,
0x6d, 0x6f, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x0b,
0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x33, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x74,
0x61, 0x72, 0x74, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x10, 0x01, 0x12,
0x0b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x10, 0x02, 0x42, 0x13, 0x5a, 0x11,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xaf, 0x03, 0x0a, 0x04,
0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, 0x65,
0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e,
0x6f, 0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77,
0x61, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d,
0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74,
0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61,
0x62, 0x65, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e,
0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f,
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c,
0x0a, 0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06,
0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73,
0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73,
0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73,
0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04,
0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d,
0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x33, 0x0a,
0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x09, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53,
0x74, 0x6f, 0x70, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74,
0x10, 0x02, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
@@ -854,36 +948,39 @@ func file_types_proto_rawDescGZIP() []byte {
}
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_types_proto_goTypes = []any{
(ContainerAction)(0), // 0: protobuf.ContainerAction
(*Container)(nil), // 1: protobuf.Container
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
(*LogEvent)(nil), // 3: protobuf.LogEvent
(*SimpleMessage)(nil), // 4: protobuf.SimpleMessage
(*ComplexMessage)(nil), // 5: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 6: protobuf.ContainerEvent
(*Host)(nil), // 7: protobuf.Host
nil, // 8: protobuf.Container.LabelsEntry
nil, // 9: protobuf.Host.LabelsEntry
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*anypb.Any)(nil), // 11: google.protobuf.Any
(*LogFragment)(nil), // 3: protobuf.LogFragment
(*LogEvent)(nil), // 4: protobuf.LogEvent
(*SingleMessage)(nil), // 5: protobuf.SingleMessage
(*GroupMessage)(nil), // 6: protobuf.GroupMessage
(*ComplexMessage)(nil), // 7: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 8: protobuf.ContainerEvent
(*Host)(nil), // 9: protobuf.Host
nil, // 10: protobuf.Container.LabelsEntry
nil, // 11: protobuf.Host.LabelsEntry
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
(*anypb.Any)(nil), // 13: google.protobuf.Any
}
var file_types_proto_depIdxs = []int32{
10, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
10, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
8, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
12, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
12, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
10, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
10, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
11, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
10, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
10, // 7: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
9, // 8: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
9, // [9:9] is the sub-list for method output_type
9, // [9:9] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
12, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
13, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
12, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
3, // 7: protobuf.GroupMessage.fragments:type_name -> protobuf.LogFragment
12, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
11, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_types_proto_init() }
@@ -897,7 +994,7 @@ func file_types_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
NumEnums: 1,
NumMessages: 9,
NumMessages: 11,
NumExtensions: 0,
NumServices: 0,
},
+13 -2
View File
@@ -435,10 +435,21 @@ func logEventToPb(event *container.LogEvent) *pb.LogEvent {
switch data := event.Message.(type) {
case string:
message, _ = anypb.New(&pb.SimpleMessage{
message, _ = anypb.New(&pb.SingleMessage{
Message: data,
})
case []container.LogFragment:
fragments := make([]*pb.LogFragment, len(data))
for i, f := range data {
fragments[i] = &pb.LogFragment{
Message: f.Message,
}
}
message, _ = anypb.New(&pb.GroupMessage{
Fragments: fragments,
})
case *orderedmap.OrderedMap[string, any]:
message, _ = anypb.New(&pb.ComplexMessage{
Data: orderedMapToJSONBytes(data),
@@ -459,7 +470,7 @@ func logEventToPb(event *container.LogEvent) *pb.LogEvent {
ContainerId: event.ContainerID,
Level: event.Level,
Stream: event.Stream,
Position: string(event.Position),
Type: string(event.Type),
RawMessage: string(event.RawMessage),
}
}
+123 -45
View File
@@ -48,38 +48,140 @@ func NewEventGenerator(ctx context.Context, reader LogReader, container Containe
return generator
}
func (g *EventGenerator) emit(event *LogEvent) bool {
select {
case g.Events <- event:
return true
case <-g.ctx.Done():
return false
}
}
func (g *EventGenerator) flushGroup(pendingGroup []*LogEvent) bool {
if len(pendingGroup) == 0 {
return true
}
if len(pendingGroup) == 1 {
pendingGroup[0].Type = LogTypeSingle
return g.emit(pendingGroup[0])
}
first := pendingGroup[0]
fragments := make([]LogFragment, len(pendingGroup))
for i, e := range pendingGroup {
fragments[i] = LogFragment{Message: e.Message.(string)}
}
return g.emit(&LogEvent{
Type: LogTypeGroup,
Message: fragments,
Timestamp: first.Timestamp,
Id: first.Id,
Level: first.Level,
Stream: first.Stream,
ContainerID: first.ContainerID,
})
}
func (g *EventGenerator) processBuffer() {
var current, next *LogEvent
var pendingGroup []*LogEvent
loop:
for {
if g.next != nil {
current = g.next
g.next = nil
next = g.peek()
} else {
event, ok := <-g.buffer
if !ok {
break loop
}
current = event
next = g.peek()
current := g.nextEvent()
if current == nil {
g.flushGroup(pendingGroup)
break loop
}
checkPosition(current, next)
// Complex logs are emitted immediately
if !current.IsSimple() {
if !g.flushGroup(pendingGroup) {
break loop
}
pendingGroup = nil
if !g.emit(current) {
break loop
}
continue
}
select {
case g.Events <- current:
case <-g.ctx.Done():
break loop
// Simple log - peek ahead to decide grouping
next := g.peek()
if len(pendingGroup) == 0 {
if next != nil && next.IsSimple() && canStartGroup(current, next) {
next.Level = current.Level
pendingGroup = append(pendingGroup, current)
} else {
current.Type = LogTypeSingle
if !g.emit(current) {
break loop
}
}
continue
}
pendingGroup = append(pendingGroup, current)
if next == nil || !next.IsSimple() || !canContinueGroup(pendingGroup[0], next) {
if !g.flushGroup(pendingGroup) {
break loop
}
pendingGroup = nil
} else {
next.Level = pendingGroup[0].Level
}
}
close(g.Events)
g.wg.Done()
}
func (g *EventGenerator) nextEvent() *LogEvent {
if g.next != nil {
event := g.next
g.next = nil
return event
}
event, ok := <-g.buffer
if !ok {
return nil
}
return event
}
// canStartGroup checks if current can start a group with next
func canStartGroup(current, next *LogEvent) bool {
// Current must have a known level
if !current.HasLevel() {
return false
}
// Next must not have its own level (continuation)
if next.HasLevel() {
return false
}
// Must be close in time
if !current.IsCloseToTime(next) {
return false
}
return true
}
// canContinueGroup checks if next can be added to a group started by first
func canContinueGroup(first, next *LogEvent) bool {
// Next must not have its own level (continuation)
if next.HasLevel() {
return false
}
// Must be close in time to the group leader
if !first.IsCloseToTime(next) {
return false
}
return true
}
func (g *EventGenerator) consumeReader() {
for {
message, streamType, readerError := g.reader.Read()
@@ -117,7 +219,7 @@ func (g *EventGenerator) peek() *LogEvent {
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
logEvent := &LogEvent{Id: h.Sum32(), Message: message, Stream: streamType.String()}
logEvent := &LogEvent{Id: h.Sum32(), Message: message, Stream: streamType.String(), Type: LogTypeSingle}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
@@ -141,10 +243,12 @@ func createEvent(message string, streamType StdType) *LogEvent {
logEvent.Message = ""
} else {
logEvent.Message = data
logEvent.Type = LogTypeComplex
}
}
} else if data, err := ParseLogFmt(message); err == nil {
logEvent.Message = data
logEvent.Type = LogTypeComplex
data, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
@@ -155,29 +259,3 @@ func createEvent(message string, streamType StdType) *LogEvent {
}
return logEvent
}
func checkPosition(currentEvent *LogEvent, nextEvent *LogEvent) {
currentLevel := guessLogLevel(currentEvent)
if nextEvent != nil {
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "unknown" && !nextEvent.HasLevel() {
currentEvent.Position = Beginning
nextEvent.Position = Middle
}
// If next item is not close to current item or has level, set current item position to end
if currentEvent.Position == Middle && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
currentEvent.Position = End
}
// If next item is close to current item and has no level, set next item position to middle
if currentEvent.Position == Middle && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
nextEvent.Position = Middle
}
// Set next item level to current item level
if currentEvent.Position == Beginning || currentEvent.Position == Middle {
nextEvent.Level = currentEvent.Level
}
} else if currentEvent.Position == Middle {
currentEvent.Position = End
}
}
+102
View File
@@ -21,6 +21,7 @@ func TestEventGenerator_Events_tty(t *testing.T) {
require.NotNil(t, event, "Expected event to not be nil, but got nil")
assert.Equal(t, input, event.Message)
assert.Equal(t, LogTypeSingle, event.Type)
}
func TestEventGenerator_Events_non_tty(t *testing.T) {
@@ -31,6 +32,7 @@ func TestEventGenerator_Events_non_tty(t *testing.T) {
require.NotNil(t, event, "Expected event to not be nil, but got nil")
assert.Equal(t, input, event.Message)
assert.Equal(t, LogTypeSingle, event.Type)
}
func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
@@ -161,3 +163,103 @@ func Test_createEvent(t *testing.T) {
})
}
}
func TestEventGenerator_ComplexLog(t *testing.T) {
input := "2020-05-13T18:55:37.772853839Z {\"level\": \"info\", \"message\": \"test\"}"
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil")
assert.Equal(t, LogTypeComplex, event.Type)
_, isMap := event.Message.(*orderedmap.OrderedMap[string, any])
assert.True(t, isMap, "Expected Message to be an ordered map")
}
func TestEventGenerator_GroupedSimpleLogs(t *testing.T) {
// Create messages with same timestamp (close enough to group) where first has level
baseTime := "2020-05-13T18:55:37.772853839Z"
messages := []string{
baseTime + " ERROR: Something went wrong",
baseTime + " at line 42",
baseTime + " in function foo",
}
reader := &mockLogReader{
messages: messages,
types: []StdType{STDERR, STDERR, STDERR},
}
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil")
assert.Equal(t, LogTypeGroup, event.Type)
fragments, ok := event.Message.([]LogFragment)
require.True(t, ok, "Expected Message to be []LogFragment")
assert.Len(t, fragments, 3)
assert.Equal(t, "ERROR: Something went wrong", fragments[0].Message)
assert.Equal(t, "at line 42", fragments[1].Message)
assert.Equal(t, "in function foo", fragments[2].Message)
}
func TestEventGenerator_SingleSimpleLog(t *testing.T) {
input := "2020-05-13T18:55:37.772853839Z INFO: Single log message"
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil")
assert.Equal(t, LogTypeSingle, event.Type)
assert.Equal(t, "INFO: Single log message", event.Message)
}
func TestEventGenerator_MixedLogs(t *testing.T) {
// Mix of complex and simple logs
messages := []string{
"2020-05-13T18:55:37.772853839Z {\"level\": \"info\"}",
"2020-05-13T18:55:38.772853839Z WARN: warning message",
}
reader := &mockLogReader{
messages: messages,
types: []StdType{STDOUT, STDOUT},
}
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
// First event should be complex
event1 := <-g.Events
require.NotNil(t, event1)
assert.Equal(t, LogTypeComplex, event1.Type)
// Second event should be single simple
event2 := <-g.Events
require.NotNil(t, event2)
assert.Equal(t, LogTypeSingle, event2.Type)
}
func TestEventGenerator_NoGroupingWhenTimestampGap(t *testing.T) {
// Messages with different timestamps (too far apart to group)
messages := []string{
"2020-05-13T18:55:37.000Z ERROR: First error",
"2020-05-13T18:55:38.000Z continuation line",
}
reader := &mockLogReader{
messages: messages,
types: []StdType{STDERR, STDERR},
}
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
// Should get two separate events (not grouped due to timestamp gap)
event1 := <-g.Events
require.NotNil(t, event1)
assert.Equal(t, LogTypeSingle, event1.Type)
event2 := <-g.Events
require.NotNil(t, event2)
assert.Equal(t, LogTypeSingle, event2.Type)
}
+25 -8
View File
@@ -147,6 +147,19 @@ const (
End LogPosition = "end"
)
type LogType string
const (
LogTypeSingle LogType = "single" // Single simple text log (no grouping)
LogTypeGroup LogType = "group" // Grouped simple logs (array of fragments)
LogTypeComplex LogType = "complex" // JSON or logfmt parsed log
)
// LogFragment represents a single line within a grouped simple log
type LogFragment struct {
Message string `json:"m"`
}
type ContainerAction string
const (
@@ -166,20 +179,24 @@ func ParseContainerAction(input string) (ContainerAction, error) {
}
type LogEvent struct {
Message any `json:"m,omitempty"`
RawMessage string `json:"rm,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"`
Position LogPosition `json:"p,omitempty"`
Stream string `json:"s,omitempty"`
ContainerID string `json:"c,omitempty"`
Type LogType `json:"t,omitempty"`
Message any `json:"m,omitempty"`
RawMessage string `json:"rm,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"`
Stream string `json:"s,omitempty"`
ContainerID string `json:"c,omitempty"`
}
func (l *LogEvent) HasLevel() bool {
return l.Level != "unknown"
}
func (l *LogEvent) IsSimple() bool {
return l.Type == LogTypeSingle || l.Type == LogTypeGroup
}
func (l *LogEvent) IsCloseToTime(other *LogEvent) bool {
return math.Abs(float64(l.Timestamp-other.Timestamp)) < 10
}
+5
View File
@@ -21,6 +21,11 @@ func EscapeHTMLValues(logEvent *container.LogEvent) {
case string:
logEvent.Message = escapeAndProcessMarkers(value)
case []container.LogFragment:
for i, fragment := range value {
value[i].Message = escapeAndProcessMarkers(fragment.Message)
}
case *orderedmap.OrderedMap[string, any]:
escapeAnyMap(value)
+10
View File
@@ -51,6 +51,16 @@ func (pm *PatternMatcher) MarkInLogEvent(logEvent *container.LogEvent) bool {
return true
}
case []container.LogFragment:
found := false
for i, fragment := range value {
if pm.Regex.MatchString(fragment.Message) {
value[i].Message = pm.Regex.ReplaceAllString(fragment.Message, pm.MarkerStart+"$0"+pm.MarkerEnd)
found = true
}
}
return found
case *orderedmap.OrderedMap[string, any]:
return pm.markMapAny(value)
+10 -7
View File
@@ -81,15 +81,18 @@ Content-Type: text/html
<pre>dev</pre>
/* snapshot: Test_handler_between_dates */
{"m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
{"t":"single","m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"t":"single","m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
/* snapshot: Test_handler_between_dates_with_everything_complex */
{"m":{"msg":"a complex log message"},"rm":"{\"msg\":\"a complex log message\"}","ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"}
{"t":"complex","m":{"msg":"a complex log message"},"rm":"{\"msg\":\"a complex log message\"}","ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"}
/* snapshot: Test_handler_between_dates_with_fill */
{"m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
{"t":"single","m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"t":"single","m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
/* snapshot: Test_handler_download_logs */
INFO Testing logs...
@@ -164,7 +167,7 @@ stdout or stderr is required
/* snapshot: Test_handler_streamLogs_happy */
:ping
data: {"m":"INFO Testing logs...\n","ts":0,"id":3835490584,"l":"info","s":"stdout","c":"123456"}
data: {"t":"single","m":"INFO Testing logs...\n","ts":0,"id":3835490584,"l":"info","s":"stdout","c":"123456"}
event: container-event
@@ -182,7 +185,7 @@ data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"
/* snapshot: Test_handler_streamLogs_happy_with_id */
:ping
data: {"m":"INFO Testing logs...","rm":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info","s":"stdout","c":"123456"}
data: {"t":"single","m":"INFO Testing logs...","rm":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info","s":"stdout","c":"123456"}
id: 1589396137772
+18 -5
View File
@@ -154,11 +154,24 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
// Format timestamp in UTC
timestamp := time.UnixMilli(event.Timestamp).UTC().Format(time.RFC3339Nano)
// Write timestamp followed by message
_, err = fmt.Fprintf(f, "%s %s\n", timestamp, event.RawMessage)
if err != nil {
log.Error().Err(err).Msgf("error writing log for container %s", c.id)
return
// Handle grouped logs
if event.Type == container.LogTypeGroup {
if fragments, ok := event.Message.([]container.LogFragment); ok {
for _, fragment := range fragments {
_, err = fmt.Fprintf(f, "%s %s\n", timestamp, fragment.Message)
if err != nil {
log.Error().Err(err).Msgf("error writing log for container %s", c.id)
return
}
}
}
} else {
// Write timestamp followed by message for single/complex logs
_, err = fmt.Fprintf(f, "%s %s\n", timestamp, event.RawMessage)
if err != nil {
log.Error().Err(err).Msgf("error writing log for container %s", c.id)
return
}
}
}
} else {
+19 -6
View File
@@ -1,10 +1,11 @@
syntax = "proto3";
option go_package = "internal/agent/pb";
package protobuf;
import "google/protobuf/timestamp.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "internal/agent/pb";
message Container {
string id = 1;
@@ -35,20 +36,32 @@ message ContainerStat {
double memoryPercent = 4;
}
message LogFragment {
string message = 1;
}
message LogEvent {
uint32 id = 1;
string containerId = 2;
google.protobuf.Any message = 3;
google.protobuf.Any message = 3; // SingleMessage, GroupMessage, or ComplexMessage
google.protobuf.Timestamp timestamp = 4;
string level = 5;
string stream = 6;
string position = 7;
string type = 7; // "single", "group", or "complex"
string rawMessage = 8;
}
message SimpleMessage { string message = 1; }
message SingleMessage {
string message = 1;
}
message ComplexMessage { bytes data = 1; }
message GroupMessage {
repeated LogFragment fragments = 1;
}
message ComplexMessage {
bytes data = 1;
}
message ContainerEvent {
string actorId = 1;