Compare commits
37 Commits
v8.14.12
...
notification
| Author | SHA1 | Date | |
|---|---|---|---|
| f393d0d3e6 | |||
| 9988271490 | |||
| 05f76c4f57 | |||
| 20c1adbd7e | |||
| d44ab349b9 | |||
| 21367bcc36 | |||
| 6ece0df082 | |||
| b087f30fb9 | |||
| 5e65e2fca5 | |||
| 2303fbdfd3 | |||
| 078f4c4064 | |||
| 5be1a55d33 | |||
| 09b1bae3cd | |||
| 2a0037a667 | |||
| e00752b0bb | |||
| 497e474471 | |||
| 753d45d7af | |||
| 9a2d6fc6e8 | |||
| 4dcaa43597 | |||
| b51ec5bfa7 | |||
| 8c8ac09521 | |||
| 2537e16b6c | |||
| a8a76e29c9 | |||
| 65b2ac9181 | |||
| ddc4e310a8 | |||
| 0a2afcf93c | |||
| 41cc1eb2ff | |||
| 2a63d300a4 | |||
| fb9265204d | |||
| 41d46f95b3 | |||
| d6ad628769 | |||
| 8c4b103468 | |||
| 9ee66dbe80 | |||
| 55aeb46262 | |||
| 1b260019a1 | |||
| e07d97a28a | |||
| 7b53cffdae |
@@ -94,7 +94,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
|
||||
@@ -147,8 +147,18 @@ The frontend uses file-based routing with these conventions:
|
||||
|
||||
- **`assets/components/`** - Vue components (auto-imported)
|
||||
- `LogViewer/`: Core log viewing components
|
||||
- `SimpleLogItem.vue`: Single-line log entries
|
||||
- `ComplexLogItem.vue`: JSON/structured log entries
|
||||
- `GroupedLogItem.vue`: Multi-line grouped log entries
|
||||
- `ContainerEventLogItem.vue`: Container lifecycle events
|
||||
- `SkippedEntriesLogItem.vue`: Placeholder for skipped logs
|
||||
- `LoadMoreLogItem.vue`: Load more historical logs
|
||||
- `ContainerViewer/`: Container-specific UI
|
||||
- `common/`: Reusable UI components
|
||||
- `BarChart.vue`: Lightweight bar chart with automatic downsampling
|
||||
- `HostCard.vue`: Host overview card with metrics
|
||||
- `MetricCard.vue`: Reusable metric display component
|
||||
- `ContainerTable.vue`: Container table with historical stat visualization
|
||||
|
||||
- **`assets/stores/`** - Pinia stores (auto-imported)
|
||||
- `config.ts`: App configuration and feature flags
|
||||
@@ -161,6 +171,7 @@ The frontend uses file-based routing with these conventions:
|
||||
- `historicalLogs.ts`: Historical log fetching
|
||||
- `logContext.ts`: Log filtering and search context
|
||||
- `storage.ts`: LocalStorage abstractions
|
||||
- `visible.ts`: Log filtering by visible keys for complex logs
|
||||
|
||||
- **`assets/modules/`** - Vue plugins
|
||||
- `router.ts`: Vue Router configuration
|
||||
@@ -190,6 +201,15 @@ The frontend uses file-based routing with these conventions:
|
||||
- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)
|
||||
- Tailwind CSS with DaisyUI for styling
|
||||
- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`
|
||||
- **Log Entry Types**: Three types of log messages supported
|
||||
- `SimpleLogEntry`: Single-line text logs (`string`)
|
||||
- `ComplexLogEntry`: Structured JSON logs (`JSONObject`)
|
||||
- `GroupedLogEntry`: Multi-line grouped logs (`string[]`)
|
||||
- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages
|
||||
- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)
|
||||
- `BarChart.vue`: Self-contained bar chart with responsive downsampling
|
||||
- Downsampling algorithm: Averages data into buckets based on available screen width
|
||||
- All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)
|
||||
|
||||
### Backend
|
||||
|
||||
@@ -211,6 +231,14 @@ The frontend uses file-based routing with these conventions:
|
||||
- Integration tests with Playwright in `e2e/`
|
||||
- Tests must run with `TZ=UTC` for consistent timestamps
|
||||
|
||||
### Container Stats & Metrics
|
||||
|
||||
- Stats are tracked using exponential moving average (EMA) with alpha=0.2
|
||||
- History stored in rolling window (300 items max) via `useSimpleRefHistory`
|
||||
- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)
|
||||
- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)
|
||||
- Stats visualization uses adaptive downsampling for performance
|
||||
|
||||
### Container Labels
|
||||
|
||||
- `dev.dozzle.name`: Custom container display name
|
||||
|
||||
@@ -8,6 +8,8 @@ export {}
|
||||
declare global {
|
||||
const DEFAULT_SETTINGS: typeof import('./stores/settings').DEFAULT_SETTINGS
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const K8sNamespace: typeof import('./stores/k8s').K8sNamespace
|
||||
const K8sOwner: typeof import('./stores/k8s').K8sOwner
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const allLevels: typeof import('./composable/logContext').allLevels
|
||||
const arrayEquals: typeof import('./utils/index').arrayEquals
|
||||
@@ -257,6 +259,7 @@ declare global {
|
||||
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
|
||||
const useInterval: typeof import('@vueuse/core').useInterval
|
||||
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
||||
const useK8sStore: typeof import('./stores/k8s').useK8sStore
|
||||
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
||||
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
||||
@@ -274,12 +277,14 @@ declare global {
|
||||
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
||||
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
||||
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
||||
const useNamespaceStream: typeof import('./composable/eventStreams').useNamespaceStream
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
||||
const useNetwork: typeof import('@vueuse/core').useNetwork
|
||||
const useNow: typeof import('@vueuse/core').useNow
|
||||
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
|
||||
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
|
||||
const useOnline: typeof import('@vueuse/core').useOnline
|
||||
const useOwnerStream: typeof import('./composable/eventStreams').useOwnerStream
|
||||
const usePageLeave: typeof import('@vueuse/core').usePageLeave
|
||||
const useParallax: typeof import('@vueuse/core').useParallax
|
||||
const useParentElement: typeof import('@vueuse/core').useParentElement
|
||||
@@ -398,6 +403,9 @@ declare global {
|
||||
export type { Host } from './stores/hosts'
|
||||
import('./stores/hosts')
|
||||
// @ts-ignore
|
||||
export type { K8sNamespace, K8sOwner } from './stores/k8s'
|
||||
import('./stores/k8s')
|
||||
// @ts-ignore
|
||||
export type { Settings } from './stores/settings'
|
||||
import('./stores/settings')
|
||||
}
|
||||
@@ -409,6 +417,8 @@ declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly K8sNamespace: UnwrapRef<typeof import('./stores/k8s')['K8sNamespace']>
|
||||
readonly K8sOwner: UnwrapRef<typeof import('./stores/k8s')['K8sOwner']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
@@ -656,6 +666,7 @@ declare module 'vue' {
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useK8sStore: UnwrapRef<typeof import('./stores/k8s')['useK8sStore']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
@@ -673,12 +684,14 @@ declare module 'vue' {
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNamespaceStream: UnwrapRef<typeof import('./composable/eventStreams')['useNamespaceStream']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly useOwnerStream: UnwrapRef<typeof import('./composable/eventStreams')['useOwnerStream']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
|
||||
@@ -12,6 +12,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Announcements: typeof import('./components/Announcements.vue')['default']
|
||||
BarChart: typeof import('./components/BarChart.vue')['default']
|
||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
||||
'Carbon:information': typeof import('~icons/carbon/information')['default']
|
||||
@@ -36,6 +37,7 @@ declare module 'vue' {
|
||||
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
|
||||
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
|
||||
ContainerPopup: typeof import('./components/ContainerPopup.vue')['default']
|
||||
ContainerStatCell: typeof import('./components/ContainerStatCell.vue')['default']
|
||||
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
|
||||
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
|
||||
DateTime: typeof import('./components/common/DateTime.vue')['default']
|
||||
@@ -44,8 +46,10 @@ 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']
|
||||
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
|
||||
HostList: typeof import('./components/HostList.vue')['default']
|
||||
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
|
||||
@@ -53,6 +57,7 @@ declare module 'vue' {
|
||||
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
|
||||
IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default']
|
||||
'Ion:ellipsisVertical': typeof import('~icons/ion/ellipsis-vertical')['default']
|
||||
K8sMenu: typeof import('./components/K8sMenu.vue')['default']
|
||||
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
|
||||
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
|
||||
Links: typeof import('./components/Links.vue')['default']
|
||||
@@ -80,6 +85,7 @@ declare module 'vue' {
|
||||
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
|
||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
||||
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
|
||||
'Mdi:close': typeof import('~icons/mdi/close')['default']
|
||||
@@ -94,13 +100,16 @@ declare module 'vue' {
|
||||
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
|
||||
MetricCard: typeof import('./components/MetricCard.vue')['default']
|
||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
||||
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
|
||||
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
|
||||
NamespaceLog: typeof import('./components/K8sViewer/NamespaceLog.vue')['default']
|
||||
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
||||
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
||||
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
|
||||
OwnerLog: typeof import('./components/K8sViewer/OwnerLog.vue')['default']
|
||||
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
|
||||
'Ph:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default']
|
||||
'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default']
|
||||
@@ -108,13 +117,13 @@ 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']
|
||||
PhArrowDown: typeof import('~icons/ph/arrow-down')['default']
|
||||
PhArrowUp: typeof import('~icons/ph/arrow-up')['default']
|
||||
Popup: typeof import('./components/Popup.vue')['default']
|
||||
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
|
||||
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
|
||||
@@ -134,7 +143,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']
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="flex items-end gap-[2px]" @mousemove="onContainerHover">
|
||||
<div
|
||||
v-for="(dataPoint, i) in downsampledData"
|
||||
:key="i"
|
||||
class="bar min-h-px flex-1 rounded-t-sm"
|
||||
:class="barClass"
|
||||
:style="{ '--height': `${Math.min(dataPoint, 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
height: var(--height);
|
||||
will-change: height;
|
||||
contain: layout;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { chartData, barClass = "" } = defineProps<{
|
||||
chartData: number[];
|
||||
barClass?: string;
|
||||
}>();
|
||||
|
||||
const hoverIndex = defineEmit<[startIndex: number, endIndex: number]>();
|
||||
|
||||
const chartContainer = ref<HTMLElement | null>(null);
|
||||
const { width } = useElementSize(chartContainer);
|
||||
|
||||
const BAR_WIDTH = 3;
|
||||
const GAP = 2;
|
||||
|
||||
const availableBars = computed(() => Math.floor(width.value / (BAR_WIDTH + GAP)));
|
||||
const bucketSize = computed(() => Math.ceil(chartData.length / availableBars.value));
|
||||
|
||||
const downsampledData = ref<number[]>([]);
|
||||
const changeCounter = ref(-1);
|
||||
|
||||
// Watch chartData changes
|
||||
watch(
|
||||
() => chartData,
|
||||
() => {
|
||||
// If changeCounter is -1, it means this is the first time the data is loaded
|
||||
if (changeCounter.value === -1) {
|
||||
recalculate();
|
||||
}
|
||||
changeCounter.value++;
|
||||
if (changeCounter.value >= bucketSize.value) {
|
||||
// Recalculate when counter reaches bucket size
|
||||
recalculate();
|
||||
changeCounter.value = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Recalculate when width changes
|
||||
watch([availableBars, bucketSize], () => {
|
||||
recalculate();
|
||||
changeCounter.value = -1;
|
||||
});
|
||||
|
||||
function recalculate() {
|
||||
if (chartData.length <= availableBars.value || availableBars.value === 0) {
|
||||
downsampledData.value = [...chartData];
|
||||
return;
|
||||
}
|
||||
|
||||
const size = bucketSize.value;
|
||||
const result = [];
|
||||
|
||||
// Create complete buckets
|
||||
const numCompleteBuckets = Math.floor(chartData.length / size);
|
||||
|
||||
for (let i = 0; i < numCompleteBuckets; i++) {
|
||||
const start = i * size;
|
||||
const end = start + size;
|
||||
const bucket = chartData.slice(start, end);
|
||||
const avg = bucket.reduce((sum, val) => sum + val, 0) / bucket.length;
|
||||
result.push(avg);
|
||||
}
|
||||
|
||||
// Show only the last N bars that fit on screen
|
||||
downsampledData.value = result.slice(-availableBars.value);
|
||||
}
|
||||
|
||||
function onContainerHover(event: MouseEvent) {
|
||||
if (!chartContainer.value) return;
|
||||
|
||||
const rect = chartContainer.value.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
// Calculate which bar the mouse is over based on position
|
||||
const barWidth = width.value / downsampledData.value.length;
|
||||
const index = Math.floor(x / barWidth);
|
||||
|
||||
// Ensure index is within bounds
|
||||
if (index < 0 || index >= downsampledData.value.length) return;
|
||||
|
||||
// Map downsampled index back to original data index range
|
||||
const numCompleteBuckets = Math.floor(chartData.length / bucketSize.value);
|
||||
const offset = Math.max(0, numCompleteBuckets - availableBars.value);
|
||||
const startIndex = (offset + index) * bucketSize.value;
|
||||
const endIndex = Math.min(startIndex + bucketSize.value - 1, chartData.length - 1);
|
||||
hoverIndex(startIndex, endIndex);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<BarChart class="h-4 flex-1" :chart-data="chartData" :bar-class="barClass" />
|
||||
<span class="w-fit text-right text-sm">{{ displayValue }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Container } from "@/models/Container";
|
||||
import type { Host } from "@/stores/hosts";
|
||||
|
||||
const { container, type, host } = defineProps<{
|
||||
container: Container;
|
||||
type: "cpu" | "mem";
|
||||
host: Host;
|
||||
}>();
|
||||
|
||||
function totalCores(): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
return host.nCPU ?? 1;
|
||||
}
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (type === "cpu") {
|
||||
const cores = totalCores();
|
||||
return container.statsHistory.map((stat) => Math.min(stat.cpu / cores, 100));
|
||||
}
|
||||
return container.statsHistory.map((stat) => Math.min(stat.memory, 100));
|
||||
});
|
||||
|
||||
const averageValue = computed(() => {
|
||||
if (type === "cpu") {
|
||||
const cores = totalCores();
|
||||
return Math.min(container.movingAverage.cpu / cores, 100);
|
||||
}
|
||||
return container.movingAverage.memory;
|
||||
});
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (type === "cpu") {
|
||||
return `${averageValue.value.toFixed(0)}%`;
|
||||
}
|
||||
return formatBytes(container.movingAverage.memoryUsage);
|
||||
});
|
||||
|
||||
const barClass = computed(() => {
|
||||
const value = averageValue.value;
|
||||
if (value <= 50) return "bg-success";
|
||||
if (value <= 70) return "bg-secondary";
|
||||
if (value <= 90) return "bg-warning";
|
||||
return "bg-error";
|
||||
});
|
||||
</script>
|
||||
@@ -53,7 +53,7 @@
|
||||
v-for="(value, key) in fields"
|
||||
:key="key"
|
||||
@click.prevent="sort(key)"
|
||||
:class="{ 'selected-sort': key === sortField }"
|
||||
:class="[value.customClass, { 'selected-sort': key === sortField }]"
|
||||
v-show="isVisible(key)"
|
||||
>
|
||||
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
|
||||
@@ -66,8 +66,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-base-300/30">
|
||||
<tr v-for="container in paginated" :key="container.id" class="hover:bg-base-100/80!">
|
||||
<td v-if="isVisible('name')">
|
||||
<tr v-for="container in paginated" :key="container.id" v-memo="[container.id]" class="hover:bg-base-100/80!">
|
||||
<td v-if="isVisible('name')" class="max-w-80 truncate">
|
||||
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
|
||||
{{ container.name }}
|
||||
</router-link>
|
||||
@@ -78,26 +78,10 @@
|
||||
<RelativeTime :date="container.created" />
|
||||
</td>
|
||||
<td v-if="isVisible('cpu')">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<progress
|
||||
class="progress h-3 w-full rounded-3xl"
|
||||
:class="getProgressColorClass(containerAverageCpu(container))"
|
||||
:value="containerAverageCpu(container)"
|
||||
max="100"
|
||||
></progress>
|
||||
<span class="w-8 text-right text-sm"> {{ containerAverageCpu(container).toFixed(0) }}% </span>
|
||||
</div>
|
||||
<ContainerStatCell :container="container" type="cpu" :host="hosts[container.host]" />
|
||||
</td>
|
||||
<td v-if="isVisible('mem')">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<progress
|
||||
class="progress h-3 w-full rounded-3xl"
|
||||
:class="getProgressColorClass(container.movingAverage.memory)"
|
||||
:value="container.movingAverage.memory"
|
||||
max="100"
|
||||
></progress>
|
||||
<span class="w-8 text-right text-sm"> {{ container.movingAverage.memory.toFixed(0) }}% </span>
|
||||
</div>
|
||||
<ContainerStatCell :container="container" type="mem" :host="hosts[container.host]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -131,7 +115,15 @@ import { toRefs } from "@vueuse/core";
|
||||
const { hosts } = useHosts();
|
||||
const selectedHost = ref(null);
|
||||
|
||||
const fields = {
|
||||
const fields: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
sortFunc: (a: Container, b: Container) => number;
|
||||
mobileVisible: boolean;
|
||||
customClass?: string;
|
||||
}
|
||||
> = {
|
||||
name: {
|
||||
label: "label.container-name",
|
||||
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
|
||||
@@ -141,26 +133,32 @@ const fields = {
|
||||
label: "label.host",
|
||||
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "w-1",
|
||||
},
|
||||
state: {
|
||||
label: "label.status",
|
||||
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "w-1",
|
||||
},
|
||||
created: {
|
||||
label: "label.created",
|
||||
sortFunc: (a: Container, b: Container) => (a.created.getTime() - b.created.getTime()) * direction.value,
|
||||
mobileVisible: true,
|
||||
customClass: "w-1",
|
||||
},
|
||||
cpu: {
|
||||
label: "label.avg-cpu",
|
||||
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "min-w-48",
|
||||
},
|
||||
mem: {
|
||||
label: "label.avg-mem",
|
||||
sortFunc: (a: Container, b: Container) => (a.movingAverage.memory - b.movingAverage.memory) * direction.value,
|
||||
sortFunc: (a: Container, b: Container) =>
|
||||
(a.movingAverage.memoryUsage - b.movingAverage.memoryUsage) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "min-w-48",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -208,27 +206,6 @@ function sort(field: keys) {
|
||||
function isVisible(field: keys) {
|
||||
return fields[field].mobileVisible || !isMobile.value;
|
||||
}
|
||||
|
||||
function getContainerCores(container: Container): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return container.cpuLimit;
|
||||
}
|
||||
const hostInfo = hosts.value[container.host];
|
||||
return hostInfo?.nCPU ?? 1;
|
||||
}
|
||||
|
||||
function containerAverageCpu(container: Container): number {
|
||||
const cores = getContainerCores(container);
|
||||
const scaledCpu = container.movingAverage.cpu / cores;
|
||||
return Math.min(scaledCpu, 100);
|
||||
}
|
||||
|
||||
function getProgressColorClass(value: number): string {
|
||||
if (value <= 70) return "progress-success";
|
||||
if (value <= 80) return "progress-secondary";
|
||||
if (value <= 90) return "progress-warning";
|
||||
return "progress-error";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -254,9 +231,6 @@ th {
|
||||
}
|
||||
|
||||
tbody td {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ScrollableView :scrollable="scrollable" v-if="container">
|
||||
<template #header v-if="showTitle">
|
||||
<div class="@container mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
|
||||
<ContainerTitle :container="container" class="mt-1 md:mt-0" />
|
||||
<ContainerTitle :container="container" />
|
||||
<MultiContainerStat
|
||||
class="ml-auto lg:hidden lg:@3xl:flex"
|
||||
:containers="[container]"
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="card bg-base-100">
|
||||
<div class="card-body flex gap-2 max-md:p-4">
|
||||
<div class="flex flex-row gap-2 overflow-hidden">
|
||||
<div class="flex items-center gap-1 truncate text-xl font-semibold">
|
||||
<HostIcon :type="host.type" class="flex-none" />
|
||||
<div class="truncate">
|
||||
{{ host.name }}
|
||||
</div>
|
||||
|
||||
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
|
||||
<carbon:warning />
|
||||
offline
|
||||
</span>
|
||||
<span
|
||||
class="badge badge-success badge-xs gap-2 p-2"
|
||||
:class="{ 'badge-warning': config.version != host.agentVersion }"
|
||||
v-else-if="host.type == 'agent'"
|
||||
title="Dozzle Agent"
|
||||
>
|
||||
{{ host.agentVersion }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="ml-auto flex flex-row flex-wrap gap-x-2 text-sm max-md:text-xs md:gap-3">
|
||||
<li class="flex items-center gap-1">
|
||||
<octicon:container-24 class="inline-block" />
|
||||
{{ $t("label.container", hostContainers.length) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 md:gap-3" v-if="stats">
|
||||
<MetricCard
|
||||
:icon="PhCpu"
|
||||
:value="stats.weighted.movingAverage.totalCPU"
|
||||
:chartData="cpuHistory"
|
||||
container-class="border-primary/40 bg-primary/20"
|
||||
text-class="text-primary"
|
||||
bar-class="bg-primary"
|
||||
:formatValue="(value) => `${value.toFixed(1)}%`"
|
||||
:label="`${host.nCPU} CPU`"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
:icon="PhMemory"
|
||||
:value="stats.weighted.movingAverage.totalMemUsage"
|
||||
:chartData="memHistory"
|
||||
container-class="border-secondary/40 bg-secondary/20"
|
||||
text-class="text-secondary"
|
||||
bar-class="bg-secondary"
|
||||
:formatValue="(value) => formatBytes(value, { decimals: 1 })"
|
||||
:label="formatBytes(host.memTotal, { decimals: 1 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Host } from "@/stores/hosts";
|
||||
import { Container } from "@/models/Container";
|
||||
// @ts-ignore
|
||||
import PhCpu from "~icons/ph/cpu";
|
||||
// @ts-ignore
|
||||
import PhMemory from "~icons/ph/memory";
|
||||
|
||||
const props = defineProps<{
|
||||
host: Host;
|
||||
}>();
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore) as unknown as {
|
||||
containers: Ref<Container[]>;
|
||||
};
|
||||
|
||||
const hostContainers = computed(() =>
|
||||
containers.value.filter((container) => container.host === props.host.id && container.state === "running"),
|
||||
);
|
||||
|
||||
function toContainerCores(container: Container): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
return props.host.nCPU ?? 1;
|
||||
}
|
||||
|
||||
type TotalStat = {
|
||||
totalCPU: number;
|
||||
totalMem: number;
|
||||
totalMemUsage: number;
|
||||
};
|
||||
|
||||
const totalStat = ref<TotalStat>({ totalCPU: 0, totalMem: 0, totalMemUsage: 0 });
|
||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||
|
||||
const cpuHistory = computed(() =>
|
||||
history.value.map((stat) => ({
|
||||
percent: stat.totalCPU,
|
||||
value: stat.totalCPU,
|
||||
})),
|
||||
);
|
||||
const memHistory = computed(() =>
|
||||
history.value.map((stat) => ({
|
||||
percent: stat.totalMem,
|
||||
value: stat.totalMemUsage,
|
||||
})),
|
||||
);
|
||||
|
||||
const stats = reactive({ mostRecent: totalStat, weighted: useExponentialMovingAverage(totalStat) });
|
||||
|
||||
watch(
|
||||
() => hostContainers.value,
|
||||
() => {
|
||||
const initial: TotalStat[] = [];
|
||||
|
||||
for (let i = 1; i <= 300; i++) {
|
||||
const stat = hostContainers.value.reduce(
|
||||
(acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
totalCPU: acc.totalCPU + item.cpu / cores,
|
||||
totalMem: acc.totalMem + item.memory,
|
||||
totalMemUsage: acc.totalMemUsage + item.memoryUsage,
|
||||
};
|
||||
},
|
||||
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
|
||||
);
|
||||
initial.push(stat);
|
||||
}
|
||||
reset({ initial: initial.reverse() });
|
||||
stats.weighted.reset(initial.at(-1)!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
useIntervalFn(() => {
|
||||
totalStat.value = hostContainers.value.reduce(
|
||||
(acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
totalCPU: acc.totalCPU + container.stat.cpu / cores,
|
||||
totalMem: acc.totalMem + container.stat.memory,
|
||||
totalMemUsage: acc.totalMemUsage + container.stat.memoryUsage,
|
||||
};
|
||||
},
|
||||
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
|
||||
);
|
||||
}, 1000);
|
||||
</script>
|
||||
@@ -1,120 +1,11 @@
|
||||
<template>
|
||||
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
|
||||
<li v-for="host in hosts" class="card bg-base-100">
|
||||
<div class="card-body grid auto-cols-auto grid-flow-col justify-between gap-4">
|
||||
<div class="flex flex-col gap-2 overflow-hidden">
|
||||
<div class="flex items-center gap-1 truncate text-xl font-semibold">
|
||||
<HostIcon :type="host.type" class="flex-none" />
|
||||
<div class="truncate">
|
||||
{{ host.name }}
|
||||
</div>
|
||||
|
||||
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
|
||||
<carbon:warning />
|
||||
offline
|
||||
</span>
|
||||
<span
|
||||
class="badge badge-success badge-xs gap-2 p-2"
|
||||
:class="{ 'badge-warning': config.version != host.agentVersion }"
|
||||
v-else-if="host.type == 'agent'"
|
||||
title="Dozzle Agent"
|
||||
>
|
||||
{{ host.agentVersion }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="flex flex-row gap-x-2 text-sm md:gap-3">
|
||||
<li class="flex items-center gap-1"><ph:cpu /> {{ host.nCPU }} <span class="max-md:hidden">CPUs</span></li>
|
||||
<li class="flex items-center gap-1">
|
||||
<ph:memory /> {{ formatBytes(host.memTotal) }}
|
||||
<span class="max-md:hidden">total</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
|
||||
<li class="flex items-center gap-1">
|
||||
<octicon:container-24 class="inline-block" />
|
||||
{{ $t("label.container", hostContainers[host.id]?.length ?? 0) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 md:gap-8" v-if="weightedStats[host.id]">
|
||||
<div
|
||||
class="radial-progress text-primary text-[0.85rem] transition-none [--size:4rem] [--thickness:0.25em] md:text-[0.9rem] md:[--size:5rem]"
|
||||
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalCPU / (host.nCPU * 100)) * 100)};`"
|
||||
role="progressbar"
|
||||
>
|
||||
{{ weightedStats[host.id].weighted.totalCPU.toFixed(0) }}%
|
||||
</div>
|
||||
<div
|
||||
class="radial-progress text-primary text-[0.85rem] transition-none [--size:4rem] [--thickness:0.25em] md:text-[0.9rem] md:[--size:5rem]"
|
||||
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalMem / host.memTotal) * 100)};`"
|
||||
role="progressbar"
|
||||
>
|
||||
{{ formatBytes(weightedStats[host.id].weighted.totalMem, { decimals: 1, short: true }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li v-for="host in hosts" :key="host.id">
|
||||
<HostCard :host="host" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore) as unknown as {
|
||||
containers: Ref<Container[]>;
|
||||
};
|
||||
|
||||
const runningContainers = computed(() => containers.value.filter((container) => container.state === "running"));
|
||||
|
||||
const { hosts } = useHosts();
|
||||
const hostContainers = computed(() => {
|
||||
const results: Record<string, Container[]> = {};
|
||||
for (const container of runningContainers.value) {
|
||||
if (!results[container.host]) {
|
||||
results[container.host] = [];
|
||||
}
|
||||
results[container.host].push(container);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
type TotalStat = {
|
||||
totalCPU: number;
|
||||
totalMem: number;
|
||||
};
|
||||
const weightedStats: Record<string, { mostRecent: TotalStat; weighted: TotalStat }> = {};
|
||||
const initWeightedStats = () => {
|
||||
for (const [host, containers] of Object.entries(hostContainers.value)) {
|
||||
const mostRecent = ref<TotalStat>({ totalCPU: 0, totalMem: 0 });
|
||||
for (const container of containers) {
|
||||
mostRecent.value.totalCPU += container.stat.cpu;
|
||||
mostRecent.value.totalMem += container.stat.memoryUsage;
|
||||
}
|
||||
weightedStats[host] = reactive({ mostRecent, weighted: useExponentialMovingAverage(mostRecent) });
|
||||
}
|
||||
};
|
||||
|
||||
watchOnce(hostContainers, initWeightedStats);
|
||||
initWeightedStats();
|
||||
|
||||
useIntervalFn(
|
||||
() => {
|
||||
for (const [host, containers] of Object.entries(hostContainers.value)) {
|
||||
const stat = { totalCPU: 0, totalMem: 0 };
|
||||
for (const container of containers) {
|
||||
stat.totalCPU += container.stat.cpu;
|
||||
stat.totalMem += container.stat.memoryUsage;
|
||||
}
|
||||
if (weightedStats[host]) {
|
||||
// TODO fix this init
|
||||
weightedStats[host].mostRecent = stat;
|
||||
}
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}"
|
||||
class="btn btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-hosts')"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
{{ hosts[sessionHost].name }}
|
||||
@@ -64,7 +64,7 @@
|
||||
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<component :is="icon" />
|
||||
{{ label.startsWith("label.") ? $t(label) : label }}
|
||||
{{ label.startsWith("label.") ? $t(label) : label }} ({{ containers.length }})
|
||||
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -73,7 +73,7 @@
|
||||
}"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-containers')"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="breadcrumbs flex-1">
|
||||
<ul>
|
||||
<li>
|
||||
<a @click.prevent="setNamespace(null)" class="link-primary">{{ $t("label.namespaces") }}</a>
|
||||
</li>
|
||||
<li v-if="selectedNamespace === 'all'">
|
||||
{{ $t("label.all-namespaces") }}
|
||||
</li>
|
||||
<li v-else-if="selectedNamespace" class="cursor-default">
|
||||
<router-link
|
||||
:to="{
|
||||
name: '/namespace/[name]',
|
||||
params: { name: selectedNamespace },
|
||||
}"
|
||||
class="btn btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
{{ selectedNamespace }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
|
||||
<ph:dots-three-vertical-bold />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a class="text-sm capitalize" @click="collapseAll()">
|
||||
<material-symbols-light:collapse-all class="w-4" />
|
||||
{{ $t("label.collapse-all") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SlideTransition :slide-right="selectedNamespace !== null">
|
||||
<template #left>
|
||||
<ul class="menu p-0">
|
||||
<li>
|
||||
<a @click.prevent="setNamespace('all')">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.all-namespaces") }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-for="ns in namespaces" :key="ns.name">
|
||||
<a @click.prevent="setNamespace(ns.name)">
|
||||
<ph:circles-four />
|
||||
{{ ns.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template #right>
|
||||
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
|
||||
<li v-for="{ name, owners } in filteredNamespaces" :key="name">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:stack />
|
||||
{{ name }} ({{ owners.length }})
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/namespace/[name]', params: { name } }"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="owner in owners" :key="`${owner.kind}-${owner.name}`">
|
||||
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li v-if="ownersWithoutNamespace.length > 0">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.owners") }} ({{ ownersWithoutNamespace.length }})
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="owner in ownersWithoutNamespace" :key="`${owner.kind}-${owner.name}`">
|
||||
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</SlideTransition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useK8sStore();
|
||||
|
||||
const { namespaces, owners } = storeToRefs(store);
|
||||
|
||||
const selectedNamespace = ref<string | null>("all");
|
||||
|
||||
const setNamespace = (namespace: string | null) => (selectedNamespace.value = namespace);
|
||||
|
||||
const filteredNamespaces = computed(() => {
|
||||
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
|
||||
return namespaces.value;
|
||||
}
|
||||
return namespaces.value.filter((ns) => ns.name === selectedNamespace.value);
|
||||
});
|
||||
|
||||
const ownersWithoutNamespace = computed(() => {
|
||||
const filtered = owners.value.filter((owner) => !owner.namespace);
|
||||
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
|
||||
return filtered;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const menu = useTemplateRef("menu");
|
||||
|
||||
const collapseAll = () => {
|
||||
const details = menu.value?.querySelectorAll("details");
|
||||
details?.forEach((detail) => (detail.open = false));
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="namespace.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<ph:stack />
|
||||
<div class="font-mono text-sm font-semibold">{{ namespace.name }}</div>
|
||||
<ContainerDropdown :containers="namespace.containers">
|
||||
{{ $t("label.container", namespace.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="namespace.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useNamespaceStream"
|
||||
:entity="namespace"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { K8sNamespace } from "@/stores/k8s";
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const { namespace, scrollable = false } = defineProps<{
|
||||
scrollable?: boolean;
|
||||
namespace: K8sNamespace;
|
||||
}>();
|
||||
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
|
||||
provideLoggingContext(
|
||||
toRef(() => namespace.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="owner.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<ph:stack-simple />
|
||||
<div class="font-mono text-sm font-semibold">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
<ContainerDropdown :containers="owner.containers">
|
||||
{{ $t("label.container", owner.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="owner.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useOwnerStream"
|
||||
:entity="owner"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { K8sOwner } from "@/stores/k8s";
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const { owner, scrollable = false } = defineProps<{
|
||||
scrollable?: boolean;
|
||||
owner: K8sOwner;
|
||||
}>();
|
||||
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
|
||||
provideLoggingContext(
|
||||
toRef(() => owner.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
@@ -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,37 @@
|
||||
<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)"
|
||||
:class="{ 'min-h-4': 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>
|
||||
@@ -44,18 +44,29 @@
|
||||
{{ $t("action.see-in-context") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="isSupported">
|
||||
<a @click="copyLogMessage()">
|
||||
<li>
|
||||
<a
|
||||
@click="copyLogMessage()"
|
||||
:disabled="!isSupported"
|
||||
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
||||
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
||||
>
|
||||
<material-symbols:content-copy />
|
||||
{{ $t("action.copy-log") }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="isSupported">
|
||||
<a @click="copyPermalink()">
|
||||
<li>
|
||||
<a
|
||||
@click="copyPermalink()"
|
||||
:disabled="!isSupported"
|
||||
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
||||
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
||||
>
|
||||
<material-symbols:link />
|
||||
{{ $t("action.copy-link") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li v-if="logEntry instanceof ComplexLogEntry">
|
||||
<a @click="showDrawer(LogDetails, { entry: logEntry })">
|
||||
<material-symbols:code-blocks-rounded />
|
||||
@@ -69,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<{
|
||||
@@ -86,10 +97,16 @@ const { copy, isSupported, copied } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
||||
async function copyLogMessage() {
|
||||
if (!isSupported.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (logEntry instanceof ComplexLogEntry) {
|
||||
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) {
|
||||
@@ -105,6 +122,9 @@ async function copyLogMessage() {
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
if (!isSupported.value) {
|
||||
return;
|
||||
}
|
||||
const url = router.resolve({
|
||||
name: "/container/[id].time.[datetime]",
|
||||
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,16 +1,30 @@
|
||||
<template>
|
||||
<div class="flex gap-1 md:gap-4">
|
||||
<StatMonitor
|
||||
:data="memoryData"
|
||||
label="mem"
|
||||
:stat-value="formatBytes(totalStat.memoryUsage)"
|
||||
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
|
||||
/>
|
||||
<div class="grid min-w-15 grid-cols-[auto_1fr] items-center gap-0.5 text-xs leading-none max-md:hidden">
|
||||
<PhArrowUp class="text-primary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
|
||||
<PhArrowDown class="text-secondary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.rx, { short: true, decimals: 1 }) }}/s</span>
|
||||
</div>
|
||||
<StatMonitor
|
||||
:data="cpuData"
|
||||
label="load"
|
||||
:icon="PhCpu"
|
||||
:stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'"
|
||||
:limit="roundCPU(limits.cpu) + ' CPU'"
|
||||
container-class="border-primary/40 bg-primary/20"
|
||||
text-class="hover:text-primary"
|
||||
bar-class="bg-primary"
|
||||
:formatter="(value: number) => value.toFixed(2) + '%'"
|
||||
/>
|
||||
<StatMonitor
|
||||
:data="memoryData"
|
||||
:icon="PhMemory"
|
||||
:stat-value="formatBytes(totalStat.memoryUsage)"
|
||||
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
|
||||
container-class="border-secondary/40 bg-secondary/20"
|
||||
text-class="hover:text-secondary"
|
||||
bar-class="bg-secondary"
|
||||
:formatter="(value: number) => formatBytes(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,106 +32,145 @@
|
||||
<script lang="ts" setup>
|
||||
import { Stat } from "@/models/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
// @ts-ignore
|
||||
import PhCpu from "~icons/ph/cpu";
|
||||
// @ts-ignore
|
||||
import PhMemory from "~icons/ph/memory";
|
||||
|
||||
const { containers } = defineProps<{
|
||||
containers: Container[];
|
||||
}>();
|
||||
|
||||
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 });
|
||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||
const { hosts } = useHosts();
|
||||
const networkRate = ref({ rx: 0, tx: 0 });
|
||||
|
||||
const roundCPU = (num: number) => (Number.isInteger(num) ? num.toFixed(0) : num.toFixed(1));
|
||||
|
||||
function toContainerCores(container: Container): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
const hostInfo = hosts.value[container.host];
|
||||
return hostInfo?.nCPU ?? 1;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => containers,
|
||||
() => {
|
||||
const initial: Stat[] = [];
|
||||
for (let i = 1; i <= 300; i++) {
|
||||
const stat = containers.reduce(
|
||||
(acc, { statsHistory }) => {
|
||||
const item = statsHistory.at(-i);
|
||||
(acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + item.cpu,
|
||||
cpu: acc.cpu + item.cpu / cores,
|
||||
memory: acc.memory + item.memory,
|
||||
memoryUsage: acc.memoryUsage + item.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0 },
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
|
||||
);
|
||||
initial.push(stat);
|
||||
}
|
||||
totalStat.value = initial[0];
|
||||
reset({ initial: initial.reverse() });
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const limits = computed(() => {
|
||||
const hostLimits = new Map<string, { cpu: number; memory: number }>();
|
||||
// Group containers by host
|
||||
const containersByHost = new Map<string, Container[]>();
|
||||
containers.forEach((container) => {
|
||||
if (!containersByHost.has(container.host)) {
|
||||
containersByHost.set(container.host, []);
|
||||
}
|
||||
containersByHost.get(container.host)!.push(container);
|
||||
});
|
||||
|
||||
for (const container of containers) {
|
||||
if (!hostLimits.has(container.host)) {
|
||||
hostLimits.set(container.host, {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
});
|
||||
}
|
||||
if (hostLimits.get(container.host)!.cpu < hosts.value[container.host].nCPU) {
|
||||
if (container.cpuLimit == 0) {
|
||||
hostLimits.get(container.host)!.cpu = hosts.value[container.host].nCPU;
|
||||
} else {
|
||||
hostLimits.get(container.host)!.cpu = hostLimits.get(container.host)!.cpu + container.cpuLimit;
|
||||
}
|
||||
}
|
||||
if (hostLimits.get(container.host)!.memory < hosts.value[container.host].memTotal) {
|
||||
if (container.memoryLimit == 0) {
|
||||
hostLimits.get(container.host)!.memory = hosts.value[container.host].memTotal;
|
||||
} else {
|
||||
hostLimits.get(container.host)!.memory = hostLimits.get(container.host)!.memory + container.memoryLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
let totalCpu = 0;
|
||||
let totalMemory = 0;
|
||||
|
||||
return hostLimits.values().reduce(
|
||||
(acc, { cpu, memory }) => {
|
||||
return {
|
||||
cpu: acc.cpu + cpu,
|
||||
memory: acc.memory + memory,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0 },
|
||||
);
|
||||
// Process each host independently
|
||||
containersByHost.forEach((hostContainers, hostId) => {
|
||||
const hostInfo = hosts.value[hostId];
|
||||
const hostTotalMemory = hostInfo?.memTotal || 0;
|
||||
const hostTotalCpu = hostInfo?.nCPU || 0;
|
||||
|
||||
// Check if any container lacks limits
|
||||
const hasUnlimitedCpu = hostContainers.some((c) => !c.cpuLimit || c.cpuLimit <= 0);
|
||||
const hasUnlimitedMemory = hostContainers.some((c) => !c.memoryLimit);
|
||||
|
||||
// Calculate CPU for this host
|
||||
if (hasUnlimitedCpu) {
|
||||
// At least one container has no limit, use host total
|
||||
totalCpu += hostTotalCpu;
|
||||
} else {
|
||||
// All containers have limits, sum them up (capped at host total)
|
||||
const sumCpu = hostContainers.reduce((sum, c) => sum + 1, 0);
|
||||
totalCpu += Math.min(sumCpu, hostTotalCpu);
|
||||
}
|
||||
|
||||
// Calculate Memory for this host
|
||||
if (hasUnlimitedMemory) {
|
||||
// At least one container has no limit, use host total
|
||||
totalMemory += hostTotalMemory;
|
||||
} else {
|
||||
// All containers have limits, sum them up (capped at host total)
|
||||
const sumMemory = hostContainers.reduce((sum, c) => sum + (c.memoryLimit || 0), 0);
|
||||
totalMemory += Math.min(sumMemory, hostTotalMemory);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cpu: totalCpu,
|
||||
memory: totalMemory,
|
||||
};
|
||||
});
|
||||
|
||||
useIntervalFn(() => {
|
||||
const previousStat = totalStat.value;
|
||||
totalStat.value = containers.reduce(
|
||||
(acc, { stat }) => {
|
||||
(acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + stat.cpu,
|
||||
memory: acc.memory + stat.memory,
|
||||
memoryUsage: acc.memoryUsage + stat.memoryUsage,
|
||||
cpu: acc.cpu + container.stat.cpu / cores,
|
||||
memory: acc.memory + container.stat.memory,
|
||||
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0 },
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
|
||||
);
|
||||
|
||||
networkRate.value = {
|
||||
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
|
||||
tx: Math.max(0, totalStat.value.networkTxTotal - previousStat.networkTxTotal),
|
||||
};
|
||||
}, 1000);
|
||||
|
||||
const cpuData = computed(() =>
|
||||
history.value.map((stat, i) => ({
|
||||
x: i,
|
||||
y: Math.max(0, stat.cpu),
|
||||
value: Math.max(0, stat.cpu).toFixed(2) + "%",
|
||||
value: Math.max(0, stat.cpu),
|
||||
})),
|
||||
);
|
||||
|
||||
const memoryData = computed(() =>
|
||||
history.value.map((stat, i) => ({
|
||||
x: i,
|
||||
y: stat.memoryUsage,
|
||||
value: formatBytes(stat.memoryUsage),
|
||||
y: stat.memory,
|
||||
value: stat.memoryUsage,
|
||||
})),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<template>
|
||||
<div class="hover:text-primary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||
<div class="border-primary overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden">
|
||||
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
|
||||
<div class="relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false" :class="textClass">
|
||||
<div class="overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden" :class="containerClass">
|
||||
<BarChart
|
||||
:chart-data="chartData"
|
||||
:bar-class="`${barClass} opacity-70 hover:opacity-100`"
|
||||
class="h-8 w-44"
|
||||
@hover-index="(startIndex: number, endIndex: number) => onHoverIndexChange(startIndex, endIndex)"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-base-200 inline-flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
|
||||
<div class="font-light uppercase">{{ label }}</div>
|
||||
<div class="font-bold select-none">
|
||||
{{ mouseOver ? (selectedPoint?.value ?? selectedPoint?.y ?? statValue) : statValue }}
|
||||
<div class="bg-base-200 flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
|
||||
<component :is="icon" class="text-sm" />
|
||||
<div class="font-bold tabular-nums select-none">
|
||||
{{ displayValue }}
|
||||
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,18 +19,48 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Component } from "vue";
|
||||
|
||||
const {
|
||||
data,
|
||||
label,
|
||||
icon,
|
||||
statValue,
|
||||
limit = -1,
|
||||
containerClass = "border-primary",
|
||||
textClass = "",
|
||||
barClass = "bg-primary",
|
||||
formatter,
|
||||
} = defineProps<{
|
||||
data: Point<unknown>[];
|
||||
label: string;
|
||||
icon: Component;
|
||||
statValue: string | number;
|
||||
limit?: string | number;
|
||||
containerClass?: string;
|
||||
textClass?: string;
|
||||
barClass?: string;
|
||||
formatter?: (value: number) => string;
|
||||
}>();
|
||||
const selectedPoint = ref<Point<unknown> | undefined>();
|
||||
const onSelectedPoint = (point: Point<unknown>) => (selectedPoint.value = point);
|
||||
|
||||
const chartData = computed(() => data.map((point) => (point.y as number) ?? 0));
|
||||
const mouseOver = ref(false);
|
||||
const hoveredRange = ref<{ start: number; end: number } | null>(null);
|
||||
|
||||
function onHoverIndexChange(startIndex: number, endIndex: number) {
|
||||
hoveredRange.value = { start: startIndex, end: endIndex };
|
||||
}
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (mouseOver.value && hoveredRange.value !== null) {
|
||||
const { start, end } = hoveredRange.value;
|
||||
const points = data.slice(start, end + 1);
|
||||
const sum = points.reduce((acc, point) => acc + ((point.value as number) ?? (point.y as number) ?? 0), 0);
|
||||
const avg = sum / points.length;
|
||||
|
||||
if (formatter) {
|
||||
return formatter(avg);
|
||||
}
|
||||
return avg.toFixed(2);
|
||||
}
|
||||
return statValue;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<svg :width="width" :height="height" @mousemove="onMove" class="group">
|
||||
<path :d="path" class="fill-primary" />
|
||||
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="stroke-secondary invisible stroke-2 group-hover:visible" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { extent } from "d3-array";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import { area, curveStep } from "d3-shape";
|
||||
|
||||
const d3 = { extent, scaleLinear, area, curveStep };
|
||||
const { data, width = 175, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
||||
const x = d3.scaleLinear().range([0, width]);
|
||||
const y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const selectedPoint = defineEmit<[value: Point<unknown>]>();
|
||||
|
||||
const shape = d3
|
||||
.area<Point<unknown>>()
|
||||
.curve(d3.curveStep)
|
||||
.x((d) => x(d.x))
|
||||
.y0(height)
|
||||
.y1((d) => y(d.y));
|
||||
|
||||
const path = computed(() => {
|
||||
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
|
||||
y.domain(d3.extent([...data, { y: 1 }], (d) => d.y) as [number, number]);
|
||||
|
||||
return shape(data) ?? "";
|
||||
});
|
||||
|
||||
let lineX = $ref(0);
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
const { offsetX } = e;
|
||||
const xValue = x.invert(offsetX);
|
||||
const index = Math.round(xValue);
|
||||
lineX = x(index);
|
||||
const point = data[index];
|
||||
selectedPoint(point);
|
||||
}
|
||||
</script>
|
||||
@@ -14,8 +14,12 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
@@ -48,8 +52,12 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
@@ -82,8 +90,12 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="rounded-lg border p-2 md:p-3" :class="containerClass">
|
||||
<div class="mb-2 flex items-center gap-1.5 text-sm font-medium" :class="textClass">
|
||||
<component :is="icon" class="text-lg" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
<div class="mb-1.5 text-lg font-semibold tabular-nums">{{ formattedValue }}</div>
|
||||
<div class="text-base-content/60 mb-1 text-xs tabular-nums max-md:hidden">
|
||||
avg {{ formatValue(average) }} • pk {{ formatValue(peak) }}
|
||||
</div>
|
||||
<BarChart class="h-8" :chartData="percentData" :barClass="barClass" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
|
||||
export interface MetricDataPoint {
|
||||
percent: number; // value 0 - 100
|
||||
value: number;
|
||||
}
|
||||
|
||||
const {
|
||||
label,
|
||||
icon,
|
||||
value,
|
||||
chartData,
|
||||
containerClass = "",
|
||||
textClass = "",
|
||||
barClass = "",
|
||||
formatValue = (v: number) => v.toString(),
|
||||
} = defineProps<{
|
||||
label: string;
|
||||
icon: Component;
|
||||
value: string | number;
|
||||
chartData: MetricDataPoint[];
|
||||
containerClass?: string;
|
||||
textClass?: string;
|
||||
barClass?: string;
|
||||
formatValue?: (value: number) => string;
|
||||
}>();
|
||||
|
||||
const percentData = computed(() => chartData.map((d) => d.percent));
|
||||
|
||||
const peak = computed(() => (chartData.length > 0 ? Math.max(...chartData.map((d) => d.value)) : 0));
|
||||
|
||||
const average = computed(() => {
|
||||
if (chartData.length === 0) return 0;
|
||||
return chartData.reduce((sum, d) => sum + d.value, 0) / chartData.length;
|
||||
});
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
if (typeof value === "string") return value;
|
||||
return formatValue(value);
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 flex-col w-full">
|
||||
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 w-full flex-col">
|
||||
<Carousel v-model="selectedCard" class="flex-1">
|
||||
<CarouselItem v-if="config.mode === 'k8s'" :title="$t('label.k8s-menu')" id="k8s">
|
||||
<K8sMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem v-if="config.mode === 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
|
||||
<SwarmMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.host-menu')" id="host">
|
||||
<HostMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.group-menu')" v-if="customGroups.length > 0" id="group">
|
||||
<GroupMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.swarm-menu')" v-if="services.length > 0" id="swarm">
|
||||
<CarouselItem v-if="config.mode !== 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
|
||||
<SwarmMenu />
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
@@ -24,13 +30,25 @@ const { ready } = storeToRefs(containerStore);
|
||||
const route = useRoute();
|
||||
const swarmStore = useSwarmStore();
|
||||
const { services, customGroups } = storeToRefs(swarmStore);
|
||||
const selectedCard = ref<"host" | "swarm" | "group">("host");
|
||||
|
||||
let defaultCard: "host" | "swarm" | "group" | "k8s";
|
||||
switch (config.mode) {
|
||||
case "k8s":
|
||||
defaultCard = "k8s";
|
||||
break;
|
||||
case "swarm":
|
||||
defaultCard = "swarm";
|
||||
break;
|
||||
default:
|
||||
defaultCard = "host";
|
||||
}
|
||||
const selectedCard = ref<"host" | "swarm" | "group" | "k8s">(defaultCard);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
if (route.meta.menu && ["host", "swarm", "group"].includes(route.meta.menu as string)) {
|
||||
selectedCard.value = route.meta.menu as "host" | "swarm" | "group";
|
||||
if (route.meta.menu && ["host", "swarm", "group", "k8s"].includes(route.meta.menu as string)) {
|
||||
selectedCard.value = route.meta.menu as "host" | "swarm" | "group" | "k8s";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:stack />
|
||||
{{ name }}
|
||||
{{ name }} ({{ services.length }})
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/stack/[name]', params: { name } }"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-services')"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
@@ -55,7 +55,7 @@
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.services") }}
|
||||
{{ $t("label.services") }} ({{ servicesWithoutStacks.length }})
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="service in servicesWithoutStacks" :key="service.name">
|
||||
|
||||
@@ -23,6 +23,9 @@ const host = useTemplateRef<HTMLDivElement>("host");
|
||||
const terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
theme: {
|
||||
background: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
const fitAddon = new FitAddon();
|
||||
@@ -110,6 +113,10 @@ onUnmounted(() => {
|
||||
& :deep(.xterm-cursor-block.xterm-cursor-blink) {
|
||||
animation-name: blink !important;
|
||||
}
|
||||
|
||||
& :deep(.xterm-selection) {
|
||||
@apply bg-primary/30;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -31,7 +32,8 @@ export function useHostStream(host: Ref<Host>): LogStreamSource {
|
||||
}
|
||||
|
||||
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
|
||||
return useLogStream(computed(() => `/api/stacks/${stack.value.name}/logs/stream`));
|
||||
const labels = computed(() => `com.docker.stack.namespace:${stack.value.name}`);
|
||||
return useLogStream(computed(() => `/api/labels/${labels.value}/logs/stream`));
|
||||
}
|
||||
|
||||
export function useGroupedStream(group: Ref<GroupedContainers>): LogStreamSource {
|
||||
@@ -48,14 +50,25 @@ export function useMergedStream(containers: Ref<Container[]>): LogStreamSource {
|
||||
}
|
||||
|
||||
export function useServiceStream(service: Ref<Service>): LogStreamSource {
|
||||
return useLogStream(computed(() => `/api/services/${service.value.name}/logs/stream`));
|
||||
const labels = computed(() => `com.docker.swarm.service.name:${service.value.name}`);
|
||||
return useLogStream(computed(() => `/api/labels/${labels.value}/logs/stream`));
|
||||
}
|
||||
|
||||
export function useNamespaceStream(namespace: Ref<{ name: string }>): LogStreamSource {
|
||||
const labels = computed(() => `namespace:${namespace.value.name}`);
|
||||
return useLogStream(computed(() => `/api/labels/${labels.value}/logs/stream`));
|
||||
}
|
||||
|
||||
export function useOwnerStream(owner: Ref<{ name: string; kind: string }>): LogStreamSource {
|
||||
const labels = computed(() => `owner.kind:${owner.value.kind},owner.name:${owner.value.name}`);
|
||||
return useLogStream(computed(() => `/api/labels/${labels.value}/logs/stream`));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -147,3 +147,7 @@ body {
|
||||
[class*="shadow-"] {
|
||||
@apply shadow-base-content/8;
|
||||
}
|
||||
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@@ -51,10 +51,13 @@ export class Container {
|
||||
public readonly group?: string,
|
||||
public health?: ContainerHealth,
|
||||
) {
|
||||
this._stat = ref(stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0 } as Stat));
|
||||
this._stat = ref(
|
||||
stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 } as Stat),
|
||||
);
|
||||
const { history } = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
|
||||
this._statsHistory = history;
|
||||
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
|
||||
const { movingAverage } = useExponentialMovingAverage(this._stat, 0.2);
|
||||
this.movingAverageStat = movingAverage;
|
||||
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
<template>
|
||||
<PageWithLinks>
|
||||
<section>
|
||||
<HostList />
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">{{ $t("label.host-count", { count: Object.keys(hosts).length }) }}</h2>
|
||||
<button @click="hostsCollapsed = !hostsCollapsed" class="btn btn-ghost btn-sm">
|
||||
<mdi:chevron-down :class="{ 'rotate-180': !hostsCollapsed }" class="transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<HostList v-show="!hostsCollapsed" />
|
||||
</Transition>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ContainerTable :containers="runningContainers"></ContainerTable>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">
|
||||
{{ $t("label.container", { count: runningContainers.length }) }}
|
||||
</h2>
|
||||
<button @click="containersCollapsed = !containersCollapsed" class="btn btn-ghost btn-sm">
|
||||
<mdi:chevron-down :class="{ 'rotate-180': !containersCollapsed }" class="transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<ContainerTable v-show="!containersCollapsed" :containers="runningContainers" />
|
||||
</Transition>
|
||||
</section>
|
||||
</PageWithLinks>
|
||||
</template>
|
||||
@@ -14,6 +32,7 @@
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { hosts } = useHosts();
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { containers, ready } = storeToRefs(containerStore) as unknown as {
|
||||
@@ -23,6 +42,10 @@ const { containers, ready } = storeToRefs(containerStore) as unknown as {
|
||||
|
||||
const runningContainers = computed(() => containers.value.filter((c) => c.state === "running"));
|
||||
|
||||
// Persist collapse state in localStorage
|
||||
const hostsCollapsed = useStorage("DOZZLE_HOSTS_COLLAPSED", false);
|
||||
const containersCollapsed = useStorage("DOZZLE_CONTAINERS_COLLAPSED", false);
|
||||
|
||||
watchEffect(() => {
|
||||
if (ready.value) {
|
||||
setTitle(t("title.dashboard", { count: runningContainers.value.length }));
|
||||
@@ -34,4 +57,16 @@ watchEffect(() => {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.collapse-enter-active,
|
||||
.collapse-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapse-enter-from,
|
||||
.collapse-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<Search />
|
||||
<NamespaceLog :namespace="namespace" :scrollable="pinnedLogs.length > 0" v-if="namespace" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute("/namespace/[name]");
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { ready } = storeToRefs(containerStore);
|
||||
|
||||
const pinnedLogsStore = usePinnedLogsStore();
|
||||
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
|
||||
|
||||
const k8sStore = useK8sStore();
|
||||
const { namespaces } = storeToRefs(k8sStore);
|
||||
const namespace = computed(() => namespaces.value.find((ns) => ns.name === route.params.name));
|
||||
|
||||
watchEffect(() => {
|
||||
if (ready.value) {
|
||||
if (namespace.value?.name) {
|
||||
setTitle(namespace.value.name);
|
||||
} else {
|
||||
setTitle("Not Found");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
menu: k8s
|
||||
</route>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<Search />
|
||||
<OwnerLog :owner="owner" :scrollable="pinnedLogs.length > 0" v-if="owner" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute("/owner/[name]");
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { ready } = storeToRefs(containerStore);
|
||||
|
||||
const pinnedLogsStore = usePinnedLogsStore();
|
||||
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
|
||||
|
||||
const k8sStore = useK8sStore();
|
||||
const { owners } = storeToRefs(k8sStore);
|
||||
const owner = computed(() => owners.value.find((o) => o.name === route.params.name));
|
||||
|
||||
watchEffect(() => {
|
||||
if (ready.value) {
|
||||
if (owner.value?.name) {
|
||||
setTitle(`${owner.value.kind}/${owner.value.name}`);
|
||||
} else {
|
||||
setTitle("Not Found");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
menu: k8s
|
||||
</route>
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<Toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </Toggle>
|
||||
|
||||
<Toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</Toggle>
|
||||
<Toggle v-model="showTimestamp">{{ $t("settings.show-timestamps") }}</Toggle>
|
||||
|
||||
<Toggle v-model="showStd">{{ $t("settings.show-std") }}</Toggle>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Config {
|
||||
base: string;
|
||||
maxLogs: number;
|
||||
hostname: string;
|
||||
mode: "server" | "swarm" | "k8s";
|
||||
hosts: Host[];
|
||||
authProvider: "simple" | "none" | "forward-proxy";
|
||||
logoutUrl?: string;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
|
||||
import { Container, GroupedContainers } from "@/models/Container";
|
||||
|
||||
export class K8sNamespace {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly containers: Container[],
|
||||
public readonly owners: K8sOwner[],
|
||||
) {
|
||||
for (const owner of owners) {
|
||||
owner.namespace = this;
|
||||
}
|
||||
}
|
||||
|
||||
get updatedAt() {
|
||||
return this.containers.map((c) => c.created).reduce((acc, date) => (date > acc ? date : acc), new Date(0));
|
||||
}
|
||||
}
|
||||
|
||||
export class K8sOwner {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly kind: string,
|
||||
public readonly containers: Container[],
|
||||
) {}
|
||||
|
||||
namespace?: K8sNamespace;
|
||||
|
||||
get updatedAt() {
|
||||
return this.containers.map((c) => c.created).reduce((acc, date) => (date > acc ? date : acc), new Date(0));
|
||||
}
|
||||
}
|
||||
|
||||
export const useK8sStore = defineStore("k8s", () => {
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore) as unknown as { containers: Ref<Container[]> };
|
||||
|
||||
const runningContainers = computed(() => containers.value.filter((c) => c.state === "running"));
|
||||
|
||||
const namespaces = computed(() => {
|
||||
const namespacedContainers: Record<string, Container[]> = {};
|
||||
for (const container of runningContainers.value) {
|
||||
const namespace = container.labels["namespace"];
|
||||
if (namespace === undefined) continue;
|
||||
namespacedContainers[namespace] ||= [];
|
||||
namespacedContainers[namespace].push(container);
|
||||
}
|
||||
|
||||
const newNamespaces: K8sNamespace[] = [];
|
||||
|
||||
for (const [name, containers] of Object.entries(namespacedContainers)) {
|
||||
const ownerGroups: Record<string, Container[]> = {};
|
||||
|
||||
for (const container of containers) {
|
||||
const ownerKind = container.labels["owner.kind"];
|
||||
const ownerName = container.labels["owner.name"];
|
||||
|
||||
if (ownerKind === undefined || ownerName === undefined) continue;
|
||||
const key = `${ownerKind}:${ownerName}`;
|
||||
ownerGroups[key] ||= [];
|
||||
ownerGroups[key].push(container);
|
||||
}
|
||||
|
||||
const newOwners: K8sOwner[] = [];
|
||||
|
||||
for (const [key, containers] of Object.entries(ownerGroups)) {
|
||||
const [kind, name] = key.split(":");
|
||||
newOwners.push(new K8sOwner(name, kind, containers));
|
||||
}
|
||||
|
||||
if (newOwners.length === 0) continue;
|
||||
|
||||
newNamespaces.push(
|
||||
new K8sNamespace(
|
||||
name,
|
||||
containers,
|
||||
newOwners.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return newNamespaces.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
const owners = computed(() => {
|
||||
const ownerGroups: Record<string, Container[]> = {};
|
||||
|
||||
for (const container of runningContainers.value) {
|
||||
const ownerKind = container.labels["owner.kind"];
|
||||
const ownerName = container.labels["owner.name"];
|
||||
const namespace = container.labels["namespace"];
|
||||
|
||||
if (ownerKind === undefined || ownerName === undefined) continue;
|
||||
if (namespace) {
|
||||
// Skip containers that are already part of a namespace
|
||||
const hasNamespace = namespaces.value.some((ns) => ns.name === namespace);
|
||||
if (hasNamespace) continue;
|
||||
}
|
||||
|
||||
const key = `${ownerKind}:${ownerName}`;
|
||||
ownerGroups[key] ||= [];
|
||||
ownerGroups[key].push(container);
|
||||
}
|
||||
|
||||
const ownersWithNamespace = namespaces.value.flatMap((ns) => ns.owners);
|
||||
|
||||
const ownersWithoutNamespace = Object.entries(ownerGroups).map(([key, containers]) => {
|
||||
const [kind, name] = key.split(":");
|
||||
return new K8sOwner(name, kind, containers);
|
||||
});
|
||||
|
||||
return [...ownersWithNamespace, ...ownersWithoutNamespace].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
const customGroups = computed(() => {
|
||||
const grouped: Record<string, Container[]> = {};
|
||||
|
||||
for (const container of runningContainers.value) {
|
||||
const group = container.customGroup;
|
||||
if (group === undefined) continue;
|
||||
grouped[group] ||= [];
|
||||
grouped[group].push(container);
|
||||
}
|
||||
|
||||
return Object.entries(grouped).map(([name, containers]) => new GroupedContainers(name, containers));
|
||||
});
|
||||
|
||||
return {
|
||||
namespaces,
|
||||
owners,
|
||||
customGroups,
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useK8sStore, import.meta.hot));
|
||||
}
|
||||
@@ -79,6 +79,20 @@ declare module 'vue-router/auto-routes' {
|
||||
{ ids: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/namespace/[name]': RouteRecordInfo<
|
||||
'/namespace/[name]',
|
||||
'/namespace/:name',
|
||||
{ name: ParamValue<true> },
|
||||
{ name: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/owner/[name]': RouteRecordInfo<
|
||||
'/owner/[name]',
|
||||
'/owner/:name',
|
||||
{ name: ParamValue<true> },
|
||||
{ name: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/service/[name]': RouteRecordInfo<
|
||||
'/service/[name]',
|
||||
'/service/:name',
|
||||
@@ -168,6 +182,18 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/namespace/[name].vue': {
|
||||
routes:
|
||||
| '/namespace/[name]'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/owner/[name].vue': {
|
||||
routes:
|
||||
| '/owner/[name]'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/service/[name].vue': {
|
||||
routes:
|
||||
| '/service/[name]'
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface ContainerStat {
|
||||
readonly cpu: number;
|
||||
readonly memory: number;
|
||||
readonly memoryUsage: number;
|
||||
readonly networkRxTotal: number;
|
||||
readonly networkTxTotal: number;
|
||||
}
|
||||
|
||||
export type ContainerJson = {
|
||||
|
||||
@@ -2,7 +2,7 @@ export function formatBytes(
|
||||
bytes: number,
|
||||
{ decimals = 2, short = false }: { decimals?: number; short?: boolean } = { decimals: 2, short: false },
|
||||
) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
if (bytes === 0) return short ? "0B" : "0 Bytes";
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
@@ -70,7 +70,7 @@ export function useExponentialMovingAverage<T extends Record<string, number>>(so
|
||||
ema.value = newValue as T;
|
||||
});
|
||||
|
||||
return ema;
|
||||
return { movingAverage: ema, reset: (value: T) => (ema.value = value) };
|
||||
}
|
||||
|
||||
interface UseSimpleRefHistoryOptions<T> {
|
||||
|
||||
@@ -115,3 +115,23 @@ By default, Dozzle only shows running containers. To see stopped containers, you
|
||||
## Is there a way to sync my settings across multiple instances of Dozzle?
|
||||
|
||||
In single-user mode, Dozzle stores the settings in the browser's local storage. This means that the settings are only available on the browser where they were set. For Dozzle to enable syncing settings across multiple instances, it needs to know who the user is. In multi-user mode, Dozzle uses the user's username to store the settings on disk and sync them across multiple instances. This information is stored in `/data` directory. If you want to sync settings across multiple instances, you need to [enable](/guide/authentication) multi-user mode and provide a username.
|
||||
|
||||
## My Dozzle instances are timing out in Swarm Mode or I'm not seeing all my Swarm nodes when behind a load balancer. How do I fix it?
|
||||
|
||||
In Swarm Mode, Dozzle instances may require their own overlay network. If you see inconsistent behavior when connecting to different Dozzle nodes, consider adding a separate overlay network which only contains the Dozzle instances, as shown below:
|
||||
|
||||
```
|
||||
services:
|
||||
logs:
|
||||
...
|
||||
networks: [ traefik, dozzle ]
|
||||
...
|
||||
|
||||
networks:
|
||||
dozzle:
|
||||
driver: overlay
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
The external network `traefik` is the overlay network which is used for the load balancer service discovery, and we've created a new `dozzle` overlay network for the Dozzle nodes to talk to one another.
|
||||
|
||||
@@ -4,8 +4,20 @@ title: What is Dozzle?
|
||||
|
||||
# What is Dozzle?
|
||||
|
||||
Dozzle is an open-source project sponsored by Docker OSS. It is a log viewer designed to simplify monitoring and debugging containers. This lightweight, web-based application offers real-time log streaming, filtering, and searching capabilities through an intuitive user interface.
|
||||
Dozzle is an open-source project sponsored by Docker OSS. It is a lightweight, web-based log viewer designed to simplify monitoring and debugging containerized applications across Docker, Docker Swarm, and Kubernetes environments.
|
||||
|
||||
Users can quickly access logs generated by their Docker containers, making it an essential tool for debugging and troubleshooting applications in a Docker environment. By default, Dozzle supports JSON logs with intelligent color coding.
|
||||
## Key Features
|
||||
|
||||
Dozzle is easy to install and configure, making it an ideal solution for developers and system administrators seeking an efficient, user-friendly log viewer for their Docker environment. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
|
||||
**Real-time Monitoring**: Stream logs from running containers with instant updates through an intuitive web interface. Monitor CPU, memory, and network usage with live metrics and historical visualizations.
|
||||
|
||||
**Flexible Deployment**: Deploy as a standalone server for single or multi-host Docker monitoring, enable automatic discovery in Docker Swarm clusters, or monitor pod logs in Kubernetes environments.
|
||||
|
||||
**Advanced Log Handling**: Automatically detects and formats JSON logs with intelligent color coding. Supports simple text logs, structured JSON logs, and multi-line grouped entries with powerful filtering and search capabilities.
|
||||
|
||||
**Multi-Host Support**: Monitor containers across multiple Docker hosts simultaneously through a distributed agent architecture using gRPC.
|
||||
|
||||
**Interactive Terminal**: Attach to running containers or execute commands directly through the web interface.
|
||||
|
||||
**Lightweight & Fast**: Built with Go backend and Vue 3 frontend, Dozzle uses efficient streaming protocols (SSE/WebSocket) and requires minimal resources.
|
||||
|
||||
Dozzle is easy to install and configure, making it an ideal solution for developers and system administrators seeking an efficient log viewer for their containerized environments. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
VM_NAME="${1:-dozzle-agent}"
|
||||
DISTRO="${2:-ubuntu}"
|
||||
AGENT_PORT="${3:-7007}"
|
||||
SHARED_CERT="./shared_cert.pem"
|
||||
SHARED_KEY="./shared_key.pem"
|
||||
|
||||
echo "🚀 Setting up Dozzle Agent on OrbStack VM: $VM_NAME"
|
||||
|
||||
# Verify shared certificates exist
|
||||
if [ ! -f "$SHARED_CERT" ]; then
|
||||
echo "❌ Shared certificate not found at $SHARED_CERT"
|
||||
echo " Run 'make generate' to create certificates"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SHARED_KEY" ]; then
|
||||
echo "❌ Shared key not found at $SHARED_KEY"
|
||||
echo " Run 'make generate' to create certificates"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found shared certificates"
|
||||
|
||||
# Step 1: Create the VM
|
||||
echo "📦 Creating VM..."
|
||||
if orb list | grep -q "^$VM_NAME"; then
|
||||
echo "⚠️ VM $VM_NAME already exists. Delete it first with: orb delete $VM_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
orb create "$DISTRO" "$VM_NAME"
|
||||
echo "✅ VM created"
|
||||
|
||||
# Wait for VM to be ready
|
||||
sleep 3
|
||||
|
||||
# Step 2: Install Docker in the VM
|
||||
echo "🐳 Installing Docker..."
|
||||
if ! orb exec -m "$VM_NAME" bash -c '
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $(whoami)
|
||||
'; then
|
||||
echo "❌ Docker installation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker installed"
|
||||
|
||||
# Step 3: Copy shared certificates to VM
|
||||
echo "🔐 Copying shared certificates to VM..."
|
||||
orb exec -m "$VM_NAME" bash -c 'mkdir -p ~/dozzle-certs'
|
||||
|
||||
echo " Copying shared_cert.pem..."
|
||||
cat "$SHARED_CERT" | orb exec -m "$VM_NAME" bash -c 'cat > ~/dozzle-certs/shared_cert.pem'
|
||||
|
||||
echo " Copying shared_key.pem..."
|
||||
cat "$SHARED_KEY" | orb exec -m "$VM_NAME" bash -c 'cat > ~/dozzle-certs/shared_key.pem'
|
||||
|
||||
echo "✅ Certificates copied"
|
||||
|
||||
# Step 4: Start Dozzle agent
|
||||
echo "🎯 Starting Dozzle agent..."
|
||||
if ! orb exec -m "$VM_NAME" bash -c "
|
||||
set -e
|
||||
docker pull amir20/dozzle:latest
|
||||
docker run -d --name dozzle-agent \
|
||||
--restart unless-stopped \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/dozzle-certs:/certs \
|
||||
-p $AGENT_PORT:7007 \
|
||||
amir20/dozzle:latest agent \
|
||||
--cert /certs/shared_cert.pem \
|
||||
--key /certs/shared_key.pem
|
||||
"; then
|
||||
echo "❌ Failed to start Dozzle agent"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Dozzle agent started"
|
||||
|
||||
# Step 5: Wait for agent to be ready
|
||||
echo "⏳ Waiting for agent to be ready..."
|
||||
sleep 3
|
||||
|
||||
# Step 6: Verify agent is running
|
||||
echo "🧪 Verifying agent is running..."
|
||||
if orb exec -m "$VM_NAME" docker ps --filter name=dozzle-agent --format "{{.Status}}" | grep -q "Up"; then
|
||||
echo "✅ Agent is running"
|
||||
else
|
||||
echo "❌ Agent failed to start. Check logs with: orb exec -m $VM_NAME docker logs dozzle-agent"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Print usage instructions
|
||||
echo ""
|
||||
echo "🎉 Setup complete!"
|
||||
echo ""
|
||||
echo "Dozzle agent is running on:"
|
||||
echo " $VM_NAME.orb.local:$AGENT_PORT"
|
||||
echo ""
|
||||
echo "To connect from your Dozzle instance, add this remote agent:"
|
||||
echo ""
|
||||
echo " docker run -v /var/run/docker.sock:/var/run/docker.sock \\"
|
||||
echo " -v $PWD/shared_cert.pem:/shared_cert.pem:ro \\"
|
||||
echo " -v $PWD/shared_key.pem:/shared_key.pem:ro \\"
|
||||
echo " -p 8080:8080 \\"
|
||||
echo " amir20/dozzle:latest \\"
|
||||
echo " --remote-agent $VM_NAME.orb.local:$AGENT_PORT \\"
|
||||
echo " --cert /shared_cert.pem --key /shared_key.pem"
|
||||
echo ""
|
||||
echo "Or use environment variables in docker-compose.yml:"
|
||||
echo ""
|
||||
echo " DOZZLE_REMOTE_AGENT: $VM_NAME.orb.local:$AGENT_PORT"
|
||||
echo " DOZZLE_CERT: /shared_cert.pem"
|
||||
echo " DOZZLE_KEY: /shared_key.pem"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " View agent logs: orb exec -m $VM_NAME docker logs -f dozzle-agent"
|
||||
echo " Stop agent: orb exec -m $VM_NAME docker stop dozzle-agent"
|
||||
echo " Start agent: orb exec -m $VM_NAME docker start dozzle-agent"
|
||||
echo " Remove agent: orb exec -m $VM_NAME docker rm -f dozzle-agent"
|
||||
echo " Delete VM: orb delete $VM_NAME"
|
||||
@@ -2,7 +2,7 @@ module github.com/amir20/dozzle
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/alexflint/go-arg v1.6.1
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
@@ -33,15 +33,15 @@ require (
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/sync v0.19.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
k8s.io/api v0.34.3
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/client-go v0.34.3
|
||||
k8s.io/metrics v0.34.3
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/metrics v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -57,6 +57,7 @@ require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -91,7 +92,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
@@ -99,20 +100,20 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
|
||||
@@ -10,6 +10,8 @@ github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8
|
||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
|
||||
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA=
|
||||
github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc=
|
||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
@@ -76,6 +78,8 @@ github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtz
|
||||
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
@@ -247,6 +251,8 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
@@ -278,6 +284,10 @@ github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
|
||||
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.14 h1:9F3UqVQdZ5GG5y6TU0l1TbbDhZmqfevaOcinQt88Qi8=
|
||||
github.com/yuin/goldmark v1.7.14/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
@@ -326,6 +336,8 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -538,6 +550,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
@@ -558,6 +572,8 @@ google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
@@ -576,6 +592,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -606,6 +624,8 @@ k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
@@ -626,6 +646,8 @@ k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
|
||||
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
@@ -646,6 +668,8 @@ k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
|
||||
@@ -654,6 +678,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4=
|
||||
k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU=
|
||||
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
|
||||
@@ -674,12 +700,18 @@ k8s.io/metrics v0.34.2 h1:zao91FNDVPRGIiHLO2vqqe21zZVPien1goyzn0hsz90=
|
||||
k8s.io/metrics v0.34.2/go.mod h1:Ydulln+8uZZctUM8yrUQX4rfq/Ay6UzsuXf24QJ37Vc=
|
||||
k8s.io/metrics v0.34.3 h1:zKco9A0q7Ibl3alcO1kqRandTt4GKwKGOBflYJTIBHc=
|
||||
k8s.io/metrics v0.34.3/go.mod h1:BWmkYCQ9x4I120OmCtMUeuXn0VTGkJLwBErneDL5aSQ=
|
||||
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
|
||||
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v6.33.1
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.3
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -1272,220 +1272,94 @@ func (x *ContainerAttachResponse) GetStdout() []byte {
|
||||
|
||||
var File_rpc_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_rpc_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x09, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x1a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x73, 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, 0xb1, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x43, 0x0a,
|
||||
0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46,
|
||||
0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x1a, 0x53, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
|
||||
0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x65,
|
||||
0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x28, 0x0a, 0x0e, 0x52, 0x65, 0x70, 0x65, 0x61,
|
||||
0x74, 0x65, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x73, 0x22, 0x4d, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x63,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
|
||||
0x22, 0xd1, 0x01, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
|
||||
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x06, 0x66,
|
||||
0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x1a,
|
||||
0x53, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x12, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x65, 0x70, 0x65, 0x61,
|
||||
0x74, 0x65, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x3a, 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a,
|
||||
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x22, 0x89, 0x01, 0x0a, 0x11, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||
0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52,
|
||||
0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x3e, 0x0a, 0x12,
|
||||
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x6f, 0x67,
|
||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xc1, 0x01, 0x0a,
|
||||
0x17, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65,
|
||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x69,
|
||||
0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x30, 0x0a, 0x05,
|
||||
0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x03, 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, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x20,
|
||||
0x0a, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x73,
|
||||
0x22, 0xbf, 0x01, 0x0a, 0x15, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42, 0x79,
|
||||
0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05,
|
||||
0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x30,
|
||||
0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x03, 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, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c,
|
||||
0x12, 0x20, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70,
|
||||
0x65, 0x73, 0x22, 0x2c, 0x0a, 0x16, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42,
|
||||
0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61,
|
||||
0x22, 0x15, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x46, 0x0a, 0x14, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x2e, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22,
|
||||
0x14, 0x0a, 0x12, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x42, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53,
|
||||
0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04,
|
||||
0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53,
|
||||
0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x48, 0x6f, 0x73,
|
||||
0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x36, 0x0a, 0x10,
|
||||
0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x12, 0x22, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04,
|
||||
0x68, 0x6f, 0x73, 0x74, 0x22, 0x1f, 0x0a, 0x1d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x53, 0x0a, 0x1e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x6d, 0x0a, 0x16, 0x43, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x19, 0x0a, 0x17, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x01, 0x0a, 0x14, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a,
|
||||
0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x05, 0x73, 0x74, 0x64,
|
||||
0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x64, 0x69,
|
||||
0x6e, 0x12, 0x31, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x65, 0x73,
|
||||
0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65,
|
||||
0x73, 0x69, 0x7a, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22,
|
||||
0x3d, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||
0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x2f,
|
||||
0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x78, 0x65, 0x63, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75,
|
||||
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x22,
|
||||
0x90, 0x01, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x74, 0x74,
|
||||
0x61, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x05,
|
||||
0x73, 0x74, 0x64, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x05, 0x73,
|
||||
0x74, 0x64, 0x69, 0x6e, 0x12, 0x31, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x00, 0x52,
|
||||
0x06, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f,
|
||||
0x61, 0x64, 0x22, 0x31, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41,
|
||||
0x74, 0x74, 0x61, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a,
|
||||
0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73,
|
||||
0x74, 0x64, 0x6f, 0x75, 0x74, 0x32, 0xa1, 0x08, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x52, 0x0a,
|
||||
0x0d, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1e,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x12, 0x4b, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x12,
|
||||
0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f,
|
||||
0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x57,
|
||||
0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44, 0x61, 0x74,
|
||||
0x65, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x6f,
|
||||
0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x52, 0x61, 0x77, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42, 0x79,
|
||||
0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42,
|
||||
0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01,
|
||||
0x12, 0x51, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x16, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x12, 0x27, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x22, 0x00, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f,
|
||||
0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74,
|
||||
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0f, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x45, 0x78, 0x65, 0x63, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5c, 0x0a, 0x0f, 0x43,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x12, 0x20,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 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,
|
||||
})
|
||||
const file_rpc_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\trpc.proto\x12\bprotobuf\x1a\vtypes.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb1\x01\n" +
|
||||
"\x15ListContainersRequest\x12C\n" +
|
||||
"\x06filter\x18\x01 \x03(\v2+.protobuf.ListContainersRequest.FilterEntryR\x06filter\x1aS\n" +
|
||||
"\vFilterEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12.\n" +
|
||||
"\x05value\x18\x02 \x01(\v2\x18.protobuf.RepeatedStringR\x05value:\x028\x01\"(\n" +
|
||||
"\x0eRepeatedString\x12\x16\n" +
|
||||
"\x06values\x18\x01 \x03(\tR\x06values\"M\n" +
|
||||
"\x16ListContainersResponse\x123\n" +
|
||||
"\n" +
|
||||
"containers\x18\x01 \x03(\v2\x13.protobuf.ContainerR\n" +
|
||||
"containers\"\xd1\x01\n" +
|
||||
"\x14FindContainerRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x12B\n" +
|
||||
"\x06filter\x18\x02 \x03(\v2*.protobuf.FindContainerRequest.FilterEntryR\x06filter\x1aS\n" +
|
||||
"\vFilterEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12.\n" +
|
||||
"\x05value\x18\x02 \x01(\v2\x18.protobuf.RepeatedStringR\x05value:\x028\x01\"J\n" +
|
||||
"\x15FindContainerResponse\x121\n" +
|
||||
"\tcontainer\x18\x01 \x01(\v2\x13.protobuf.ContainerR\tcontainer\"\x89\x01\n" +
|
||||
"\x11StreamLogsRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x120\n" +
|
||||
"\x05since\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05since\x12 \n" +
|
||||
"\vstreamTypes\x18\x03 \x01(\x05R\vstreamTypes\">\n" +
|
||||
"\x12StreamLogsResponse\x12(\n" +
|
||||
"\x05event\x18\x01 \x01(\v2\x12.protobuf.LogEventR\x05event\"\xc1\x01\n" +
|
||||
"\x17LogsBetweenDatesRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x120\n" +
|
||||
"\x05since\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05since\x120\n" +
|
||||
"\x05until\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05until\x12 \n" +
|
||||
"\vstreamTypes\x18\x04 \x01(\x05R\vstreamTypes\"\xbf\x01\n" +
|
||||
"\x15StreamRawBytesRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x120\n" +
|
||||
"\x05since\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05since\x120\n" +
|
||||
"\x05until\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05until\x12 \n" +
|
||||
"\vstreamTypes\x18\x04 \x01(\x05R\vstreamTypes\",\n" +
|
||||
"\x16StreamRawBytesResponse\x12\x12\n" +
|
||||
"\x04data\x18\x01 \x01(\fR\x04data\"\x15\n" +
|
||||
"\x13StreamEventsRequest\"F\n" +
|
||||
"\x14StreamEventsResponse\x12.\n" +
|
||||
"\x05event\x18\x01 \x01(\v2\x18.protobuf.ContainerEventR\x05event\"\x14\n" +
|
||||
"\x12StreamStatsRequest\"B\n" +
|
||||
"\x13StreamStatsResponse\x12+\n" +
|
||||
"\x04stat\x18\x01 \x01(\v2\x17.protobuf.ContainerStatR\x04stat\"\x11\n" +
|
||||
"\x0fHostInfoRequest\"6\n" +
|
||||
"\x10HostInfoResponse\x12\"\n" +
|
||||
"\x04host\x18\x01 \x01(\v2\x0e.protobuf.HostR\x04host\"\x1f\n" +
|
||||
"\x1dStreamContainerStartedRequest\"S\n" +
|
||||
"\x1eStreamContainerStartedResponse\x121\n" +
|
||||
"\tcontainer\x18\x01 \x01(\v2\x13.protobuf.ContainerR\tcontainer\"m\n" +
|
||||
"\x16ContainerActionRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x121\n" +
|
||||
"\x06action\x18\x02 \x01(\x0e2\x19.protobuf.ContainerActionR\x06action\"\x19\n" +
|
||||
"\x17ContainerActionResponse\"\xa8\x01\n" +
|
||||
"\x14ContainerExecRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x12\x18\n" +
|
||||
"\acommand\x18\x02 \x03(\tR\acommand\x12\x16\n" +
|
||||
"\x05stdin\x18\x03 \x01(\fH\x00R\x05stdin\x121\n" +
|
||||
"\x06resize\x18\x04 \x01(\v2\x17.protobuf.ResizePayloadH\x00R\x06resizeB\t\n" +
|
||||
"\apayload\"=\n" +
|
||||
"\rResizePayload\x12\x14\n" +
|
||||
"\x05width\x18\x01 \x01(\rR\x05width\x12\x16\n" +
|
||||
"\x06height\x18\x02 \x01(\rR\x06height\"/\n" +
|
||||
"\x15ContainerExecResponse\x12\x16\n" +
|
||||
"\x06stdout\x18\x01 \x01(\fR\x06stdout\"\x90\x01\n" +
|
||||
"\x16ContainerAttachRequest\x12 \n" +
|
||||
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x12\x16\n" +
|
||||
"\x05stdin\x18\x02 \x01(\fH\x00R\x05stdin\x121\n" +
|
||||
"\x06resize\x18\x03 \x01(\v2\x17.protobuf.ResizePayloadH\x00R\x06resizeB\t\n" +
|
||||
"\apayload\"1\n" +
|
||||
"\x17ContainerAttachResponse\x12\x16\n" +
|
||||
"\x06stdout\x18\x01 \x01(\fR\x06stdout2\xa1\b\n" +
|
||||
"\fAgentService\x12U\n" +
|
||||
"\x0eListContainers\x12\x1f.protobuf.ListContainersRequest\x1a .protobuf.ListContainersResponse\"\x00\x12R\n" +
|
||||
"\rFindContainer\x12\x1e.protobuf.FindContainerRequest\x1a\x1f.protobuf.FindContainerResponse\"\x00\x12K\n" +
|
||||
"\n" +
|
||||
"StreamLogs\x12\x1b.protobuf.StreamLogsRequest\x1a\x1c.protobuf.StreamLogsResponse\"\x000\x01\x12W\n" +
|
||||
"\x10LogsBetweenDates\x12!.protobuf.LogsBetweenDatesRequest\x1a\x1c.protobuf.StreamLogsResponse\"\x000\x01\x12W\n" +
|
||||
"\x0eStreamRawBytes\x12\x1f.protobuf.StreamRawBytesRequest\x1a .protobuf.StreamRawBytesResponse\"\x000\x01\x12Q\n" +
|
||||
"\fStreamEvents\x12\x1d.protobuf.StreamEventsRequest\x1a\x1e.protobuf.StreamEventsResponse\"\x000\x01\x12N\n" +
|
||||
"\vStreamStats\x12\x1c.protobuf.StreamStatsRequest\x1a\x1d.protobuf.StreamStatsResponse\"\x000\x01\x12o\n" +
|
||||
"\x16StreamContainerStarted\x12'.protobuf.StreamContainerStartedRequest\x1a(.protobuf.StreamContainerStartedResponse\"\x000\x01\x12C\n" +
|
||||
"\bHostInfo\x12\x19.protobuf.HostInfoRequest\x1a\x1a.protobuf.HostInfoResponse\"\x00\x12X\n" +
|
||||
"\x0fContainerAction\x12 .protobuf.ContainerActionRequest\x1a!.protobuf.ContainerActionResponse\"\x00\x12V\n" +
|
||||
"\rContainerExec\x12\x1e.protobuf.ContainerExecRequest\x1a\x1f.protobuf.ContainerExecResponse\"\x00(\x010\x01\x12\\\n" +
|
||||
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_rpc_proto_rawDescOnce sync.Once
|
||||
|
||||
@@ -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-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.3
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -266,40 +266,40 @@ type AgentServiceServer interface {
|
||||
type UnimplementedAgentServiceServer struct{}
|
||||
|
||||
func (UnimplementedAgentServiceServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListContainers not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) FindContainer(context.Context, *FindContainerRequest) (*FindContainerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FindContainer not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FindContainer not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) StreamLogs(*StreamLogsRequest, grpc.ServerStreamingServer[StreamLogsResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamLogs not implemented")
|
||||
return status.Error(codes.Unimplemented, "method StreamLogs not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) LogsBetweenDates(*LogsBetweenDatesRequest, grpc.ServerStreamingServer[StreamLogsResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method LogsBetweenDates not implemented")
|
||||
return status.Error(codes.Unimplemented, "method LogsBetweenDates not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) StreamRawBytes(*StreamRawBytesRequest, grpc.ServerStreamingServer[StreamRawBytesResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamRawBytes not implemented")
|
||||
return status.Error(codes.Unimplemented, "method StreamRawBytes not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[StreamEventsResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamEvents not implemented")
|
||||
return status.Error(codes.Unimplemented, "method StreamEvents not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) StreamStats(*StreamStatsRequest, grpc.ServerStreamingServer[StreamStatsResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamStats not implemented")
|
||||
return status.Error(codes.Unimplemented, "method StreamStats not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) StreamContainerStarted(*StreamContainerStartedRequest, grpc.ServerStreamingServer[StreamContainerStartedResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamContainerStarted not implemented")
|
||||
return status.Error(codes.Unimplemented, "method StreamContainerStarted not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) HostInfo(context.Context, *HostInfoRequest) (*HostInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HostInfo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method HostInfo not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ContainerAction not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ContainerAction not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ContainerExec not implemented")
|
||||
return status.Error(codes.Unimplemented, "method ContainerExec not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ContainerAttach not implemented")
|
||||
return status.Error(codes.Unimplemented, "method ContainerAttach not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {}
|
||||
func (UnimplementedAgentServiceServer) testEmbeddedByValue() {}
|
||||
@@ -312,7 +312,7 @@ type UnsafeAgentServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterAgentServiceServer(s grpc.ServiceRegistrar, srv AgentServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAgentServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAgentServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@@ -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-gen-go v1.36.11
|
||||
// protoc v6.33.3
|
||||
// source: types.proto
|
||||
|
||||
package pb
|
||||
@@ -261,13 +261,15 @@ func (x *Container) GetFullyLoaded() bool {
|
||||
}
|
||||
|
||||
type ContainerStat struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
|
||||
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
|
||||
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
|
||||
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
|
||||
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
|
||||
NetworkRxTotal uint64 `protobuf:"varint,5,opt,name=networkRxTotal,proto3" json:"networkRxTotal,omitempty"`
|
||||
NetworkTxTotal uint64 `protobuf:"varint,6,opt,name=networkTxTotal,proto3" json:"networkTxTotal,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ContainerStat) Reset() {
|
||||
@@ -328,15 +330,73 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ContainerStat) GetNetworkRxTotal() uint64 {
|
||||
if x != nil {
|
||||
return x.NetworkRxTotal
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ContainerStat) GetNetworkTxTotal() uint64 {
|
||||
if x != nil {
|
||||
return x.NetworkTxTotal
|
||||
}
|
||||
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 +404,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 +416,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 +429,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 +474,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 +488,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 +520,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 +585,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 +597,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 +610,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 +632,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 +644,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 +657,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 +708,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 +720,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 +733,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 {
|
||||
@@ -718,128 +822,87 @@ func (x *Host) GetDockerVersion() string {
|
||||
|
||||
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,
|
||||
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,
|
||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x49, 0x6d, 0x61,
|
||||
0x67, 0x65, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x49, 0x6d, 0x61, 0x67,
|
||||
0x65, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x07,
|
||||
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, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x65, 0x64, 0x18, 0x08, 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, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x12,
|
||||
0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18,
|
||||
0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74,
|
||||
0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x74, 0x74, 0x79, 0x12, 0x37, 0x0a,
|
||||
0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06,
|
||||
0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18,
|
||||
0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x52, 0x05,
|
||||
0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x0e,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x36, 0x0a, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65,
|
||||
0x64, 0x18, 0x10, 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, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x20, 0x0a,
|
||||
0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x11, 0x20, 0x01,
|
||||
0x28, 0x04, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12,
|
||||
0x1a, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28,
|
||||
0x01, 0x52, 0x08, 0x63, 0x70, 0x75, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x66,
|
||||
0x75, 0x6c, 0x6c, 0x79, 0x4c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x4c, 0x6f, 0x61, 0x64, 0x65, 0x64, 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, 0x22, 0x87, 0x01, 0x0a, 0x0d, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x70,
|
||||
0x75, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a,
|
||||
0x63, 0x70, 0x75, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65,
|
||||
0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52,
|
||||
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,
|
||||
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,
|
||||
})
|
||||
const file_types_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vtypes.proto\x12\bprotobuf\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa2\x05\n" +
|
||||
"\tContainer\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05image\x18\x03 \x01(\tR\x05image\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\x12\x14\n" +
|
||||
"\x05state\x18\x05 \x01(\tR\x05state\x12\x18\n" +
|
||||
"\aImageId\x18\x06 \x01(\tR\aImageId\x124\n" +
|
||||
"\acreated\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x124\n" +
|
||||
"\astarted\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\astarted\x12\x16\n" +
|
||||
"\x06health\x18\t \x01(\tR\x06health\x12\x12\n" +
|
||||
"\x04host\x18\n" +
|
||||
" \x01(\tR\x04host\x12\x10\n" +
|
||||
"\x03tty\x18\v \x01(\bR\x03tty\x127\n" +
|
||||
"\x06labels\x18\f \x03(\v2\x1f.protobuf.Container.LabelsEntryR\x06labels\x12-\n" +
|
||||
"\x05stats\x18\r \x03(\v2\x17.protobuf.ContainerStatR\x05stats\x12\x14\n" +
|
||||
"\x05group\x18\x0e \x01(\tR\x05group\x12\x18\n" +
|
||||
"\acommand\x18\x0f \x01(\tR\acommand\x126\n" +
|
||||
"\bfinished\x18\x10 \x01(\v2\x1a.google.protobuf.TimestampR\bfinished\x12 \n" +
|
||||
"\vmemoryLimit\x18\x11 \x01(\x04R\vmemoryLimit\x12\x1a\n" +
|
||||
"\bcpuLimit\x18\x12 \x01(\x01R\bcpuLimit\x12 \n" +
|
||||
"\vfullyLoaded\x18\x13 \x01(\bR\vfullyLoaded\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd7\x01\n" +
|
||||
"\rContainerStat\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1e\n" +
|
||||
"\n" +
|
||||
"cpuPercent\x18\x02 \x01(\x01R\n" +
|
||||
"cpuPercent\x12 \n" +
|
||||
"\vmemoryUsage\x18\x03 \x01(\x01R\vmemoryUsage\x12$\n" +
|
||||
"\rmemoryPercent\x18\x04 \x01(\x01R\rmemoryPercent\x12&\n" +
|
||||
"\x0enetworkRxTotal\x18\x05 \x01(\x04R\x0enetworkRxTotal\x12&\n" +
|
||||
"\x0enetworkTxTotal\x18\x06 \x01(\x04R\x0enetworkTxTotal\"'\n" +
|
||||
"\vLogFragment\x12\x18\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage\"\x88\x02\n" +
|
||||
"\bLogEvent\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\x12 \n" +
|
||||
"\vcontainerId\x18\x02 \x01(\tR\vcontainerId\x12.\n" +
|
||||
"\amessage\x18\x03 \x01(\v2\x14.google.protobuf.AnyR\amessage\x128\n" +
|
||||
"\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x14\n" +
|
||||
"\x05level\x18\x05 \x01(\tR\x05level\x12\x16\n" +
|
||||
"\x06stream\x18\x06 \x01(\tR\x06stream\x12\x12\n" +
|
||||
"\x04type\x18\a \x01(\tR\x04type\x12\x1e\n" +
|
||||
"\n" +
|
||||
"rawMessage\x18\b \x01(\tR\n" +
|
||||
"rawMessage\")\n" +
|
||||
"\rSingleMessage\x12\x18\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage\"C\n" +
|
||||
"\fGroupMessage\x123\n" +
|
||||
"\tfragments\x18\x01 \x03(\v2\x15.protobuf.LogFragmentR\tfragments\"$\n" +
|
||||
"\x0eComplexMessage\x12\x12\n" +
|
||||
"\x04data\x18\x01 \x01(\fR\x04data\"\x8c\x01\n" +
|
||||
"\x0eContainerEvent\x12\x18\n" +
|
||||
"\aactorId\x18\x01 \x01(\tR\aactorId\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04host\x18\x03 \x01(\tR\x04host\x128\n" +
|
||||
"\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\xaf\x03\n" +
|
||||
"\x04Host\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12 \n" +
|
||||
"\vnodeAddress\x18\x03 \x01(\tR\vnodeAddress\x12\x14\n" +
|
||||
"\x05swarm\x18\x04 \x01(\bR\x05swarm\x122\n" +
|
||||
"\x06labels\x18\x05 \x03(\v2\x1a.protobuf.Host.LabelsEntryR\x06labels\x12(\n" +
|
||||
"\x0foperatingSystem\x18\x06 \x01(\tR\x0foperatingSystem\x12\x1c\n" +
|
||||
"\tosVersion\x18\a \x01(\tR\tosVersion\x12\x16\n" +
|
||||
"\x06osType\x18\b \x01(\tR\x06osType\x12\x1a\n" +
|
||||
"\bcpuCores\x18\t \x01(\rR\bcpuCores\x12\x16\n" +
|
||||
"\x06memory\x18\n" +
|
||||
" \x01(\x04R\x06memory\x12\"\n" +
|
||||
"\fagentVersion\x18\v \x01(\tR\fagentVersion\x12$\n" +
|
||||
"\rdockerVersion\x18\f \x01(\tR\rdockerVersion\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*3\n" +
|
||||
"\x0fContainerAction\x12\t\n" +
|
||||
"\x05Start\x10\x00\x12\b\n" +
|
||||
"\x04Stop\x10\x01\x12\v\n" +
|
||||
"\aRestart\x10\x02B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_types_proto_rawDescOnce sync.Once
|
||||
@@ -854,36 +917,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 +963,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,
|
||||
},
|
||||
|
||||
@@ -171,10 +171,12 @@ func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_Stre
|
||||
case stat := <-stats:
|
||||
out.Send(&pb.StreamStatsResponse{
|
||||
Stat: &pb.ContainerStat{
|
||||
Id: stat.ID,
|
||||
CpuPercent: stat.CPUPercent,
|
||||
MemoryPercent: stat.MemoryPercent,
|
||||
MemoryUsage: stat.MemoryUsage,
|
||||
Id: stat.ID,
|
||||
CpuPercent: stat.CPUPercent,
|
||||
MemoryPercent: stat.MemoryPercent,
|
||||
MemoryUsage: stat.MemoryUsage,
|
||||
NetworkRxTotal: stat.NetworkRxTotal,
|
||||
NetworkTxTotal: stat.NetworkTxTotal,
|
||||
},
|
||||
})
|
||||
case <-out.Context().Done():
|
||||
@@ -435,10 +437,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 +472,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -64,13 +64,17 @@ func guessLogLevel(logEvent *LogEvent) string {
|
||||
}
|
||||
|
||||
// Look for the level in the middle of the message that are uppercase and surrounded by quotes
|
||||
if strings.Contains(value, "\""+strings.ToUpper(first)+"\"") {
|
||||
return first
|
||||
for _, level := range levelGroup {
|
||||
if strings.Contains(value, "\""+strings.ToUpper(level)+"\"") {
|
||||
return first
|
||||
}
|
||||
}
|
||||
|
||||
// Look for the level in the middle of the message that are uppercase
|
||||
if strings.Contains(value, " "+strings.ToUpper(first)+" ") {
|
||||
return first
|
||||
for _, level := range levelGroup {
|
||||
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
|
||||
return first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ func TestGuessLogLevel(t *testing.T) {
|
||||
{"inf Something went wrong", "info"},
|
||||
{"crit: Something went wrong", "fatal"},
|
||||
{"[21:01:45] [WRN] this is a test", "warn"},
|
||||
{"2026-01-05 12:13:24,566 - retry.api (7fd8ad34eb30) : WARNING (api:40) - HTTPSConnectionPool(host='podnapisi.net', port=443): Max retries exceeded", "warn"},
|
||||
{"2026-01-05 08:21:16,511 - root (7fd8bf822b30) : INFO (get_providers:408) - Throttling podnapisi for 10 minutes", "info"},
|
||||
{orderedmap.New[string, string](
|
||||
orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, string]{Key: "key", Value: "value"},
|
||||
|
||||
@@ -98,10 +98,12 @@ func FromProto(c *pb.Container) Container {
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
type ContainerStat struct {
|
||||
ID string `json:"id"`
|
||||
CPUPercent float64 `json:"cpu"`
|
||||
MemoryPercent float64 `json:"memory"`
|
||||
MemoryUsage float64 `json:"memoryUsage"`
|
||||
ID string `json:"id"`
|
||||
CPUPercent float64 `json:"cpu"`
|
||||
MemoryPercent float64 `json:"memory"`
|
||||
MemoryUsage float64 `json:"memoryUsage"`
|
||||
NetworkRxTotal uint64 `json:"networkRxTotal"`
|
||||
NetworkTxTotal uint64 `json:"networkTxTotal"`
|
||||
}
|
||||
|
||||
// ContainerEvent represents events that are triggered
|
||||
@@ -147,6 +149,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 +181,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
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
mem, memLimit float64
|
||||
previousCPU uint64
|
||||
previousSystem uint64
|
||||
networkRx, networkTx uint64
|
||||
)
|
||||
daemonOSType := response.OSType
|
||||
|
||||
@@ -213,15 +214,23 @@ func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
mem = float64(v.MemoryStats.PrivateWorkingSet)
|
||||
}
|
||||
|
||||
// Calculate total network bytes across all interfaces
|
||||
for _, netStats := range v.Networks {
|
||||
networkRx += netStats.RxBytes
|
||||
networkTx += netStats.TxBytes
|
||||
}
|
||||
|
||||
if cpuPercent > 0 || mem > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case stats <- container.ContainerStat{
|
||||
ID: id,
|
||||
CPUPercent: cpuPercent,
|
||||
MemoryPercent: memPercent,
|
||||
MemoryUsage: mem,
|
||||
ID: id,
|
||||
CPUPercent: cpuPercent,
|
||||
MemoryPercent: memPercent,
|
||||
MemoryUsage: mem,
|
||||
NetworkRxTotal: networkRx,
|
||||
NetworkTxTotal: networkTx,
|
||||
}:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,21 @@ func podToContainers(pod *corev1.Pod) []container.Container {
|
||||
if pod.Status.StartTime != nil {
|
||||
started = pod.Status.StartTime.Time
|
||||
}
|
||||
|
||||
// Build labels map with pod labels, namespace, and owner reference
|
||||
labels := make(map[string]string)
|
||||
for k, v := range pod.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
labels["namespace"] = pod.Namespace
|
||||
|
||||
// Add owner reference if present
|
||||
if len(pod.OwnerReferences) > 0 {
|
||||
owner := pod.OwnerReferences[0]
|
||||
labels["owner.kind"] = owner.Kind
|
||||
labels["owner.name"] = owner.Name
|
||||
}
|
||||
|
||||
var containers []container.Container
|
||||
for _, c := range pod.Spec.Containers {
|
||||
containers = append(containers, container.Container{
|
||||
@@ -106,6 +121,7 @@ func podToContainers(pod *corev1.Pod) []container.Container {
|
||||
Command: strings.Join(c.Command, " "),
|
||||
Host: pod.Spec.NodeName,
|
||||
Tty: c.TTY,
|
||||
Labels: labels,
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300),
|
||||
FullyLoaded: true,
|
||||
})
|
||||
@@ -277,7 +293,8 @@ func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.Con
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
||||
panic("not implemented")
|
||||
// Stats collection is implemented in stats_collector.go using K8s metrics API
|
||||
panic("not implemented - use K8sStatsCollector instead")
|
||||
}
|
||||
|
||||
func (k *K8sClient) Ping(ctx context.Context) error {
|
||||
|
||||
@@ -105,9 +105,11 @@ func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
|
||||
for _, pod := range metricList.Items {
|
||||
for _, c := range pod.Containers {
|
||||
stat := container.ContainerStat{
|
||||
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
|
||||
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
|
||||
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
|
||||
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
|
||||
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
|
||||
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
|
||||
NetworkRxTotal: 0, // K8s metrics API doesn't expose network stats by default
|
||||
NetworkTxTotal: 0, // Would require custom metrics or cAdvisor integration
|
||||
}
|
||||
log.Trace().Interface("stat", stat).Msg("k8s stats")
|
||||
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Manager handles notification subscriptions and matching
|
||||
type Manager struct {
|
||||
subscriptions []*compiledSubscription
|
||||
mu sync.RWMutex
|
||||
activeStreams map[string]*activeStream // containerID -> stream info
|
||||
containerCache map[string]Container // containerID -> notification.Container
|
||||
containerService ContainerService
|
||||
dispatcher Dispatcher
|
||||
}
|
||||
|
||||
// activeStream tracks a single log stream and all subscriptions watching it
|
||||
type activeStream struct {
|
||||
container Container // notification.Container
|
||||
subscriptions []*compiledSubscription // All subscriptions watching this container
|
||||
cancel context.CancelFunc // Cancel function for the stream
|
||||
}
|
||||
|
||||
// compiledSubscription wraps a Subscription with compiled expr programs
|
||||
type compiledSubscription struct {
|
||||
subscription *Subscription
|
||||
containerProgram *vm.Program // Always non-nil
|
||||
logProgram *vm.Program // Always non-nil
|
||||
}
|
||||
|
||||
// ContainerService provides access to containers and log streaming
|
||||
type ContainerService interface {
|
||||
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
|
||||
FindContainer(host, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter)
|
||||
}
|
||||
|
||||
// NewManager creates a new notification manager
|
||||
func NewManager(containerService ContainerService, dispatcher Dispatcher) *Manager {
|
||||
return &Manager{
|
||||
subscriptions: make([]*compiledSubscription, 0),
|
||||
activeStreams: make(map[string]*activeStream),
|
||||
containerCache: make(map[string]Container),
|
||||
containerService: containerService,
|
||||
dispatcher: dispatcher,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSubscriptions loads and compiles multiple subscriptions (replaces existing)
|
||||
func (m *Manager) LoadSubscriptions(subs []*Subscription) error {
|
||||
m.mu.Lock()
|
||||
m.subscriptions = make([]*compiledSubscription, 0, len(subs))
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := m.AddSubscription(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Int("count", len(subs)).Msg("loaded notification subscriptions")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSubscription adds a single subscription
|
||||
func (m *Manager) AddSubscription(sub *Subscription) error {
|
||||
cs, err := m.compileSubscription(sub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.subscriptions = append(m.subscriptions, cs)
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Info().Str("subscription", sub.Name).Msg("added notification subscription")
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileSubscription compiles expr programs for a subscription
|
||||
func (m *Manager) compileSubscription(sub *Subscription) (*compiledSubscription, error) {
|
||||
if sub.ContainerFilter == "" {
|
||||
return nil, fmt.Errorf("container_filter is required for subscription %q", sub.Name)
|
||||
}
|
||||
if sub.LogFilter == "" {
|
||||
return nil, fmt.Errorf("log_filter is required for subscription %q", sub.Name)
|
||||
}
|
||||
|
||||
cs := &compiledSubscription{
|
||||
subscription: sub,
|
||||
}
|
||||
|
||||
// Compile container filter (required)
|
||||
containerProgram, err := expr.Compile(sub.ContainerFilter, expr.Env(Container{}))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("subscription", sub.Name).Msg("failed to compile container filter")
|
||||
return nil, fmt.Errorf("failed to compile container_filter for %q: %w", sub.Name, err)
|
||||
}
|
||||
cs.containerProgram = containerProgram
|
||||
|
||||
// Compile log filter (required)
|
||||
logProgram, err := expr.Compile(sub.LogFilter, expr.Env(Log{}))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("subscription", sub.Name).Msg("failed to compile log filter")
|
||||
return nil, fmt.Errorf("failed to compile log_filter for %q: %w", sub.Name, err)
|
||||
}
|
||||
cs.logProgram = logProgram
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// Start begins monitoring containers and streaming logs for matching subscriptions
|
||||
// Only starts if there are subscriptions configured
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
m.mu.RLock()
|
||||
hasSubscriptions := len(m.subscriptions) > 0
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !hasSubscriptions {
|
||||
log.Debug().Msg("no subscriptions configured, skipping notification manager start")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe to new containers that match our filters
|
||||
newContainers := make(chan container.Container)
|
||||
m.containerService.SubscribeContainersStarted(ctx, newContainers, func(c *container.Container) bool {
|
||||
matchingSubs := m.getMatchingSubscriptions(c)
|
||||
return len(matchingSubs) > 0
|
||||
})
|
||||
|
||||
// Handle new containers in background
|
||||
go func() {
|
||||
for c := range newContainers {
|
||||
m.startContainerStream(ctx, &c)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get all existing containers and start streaming matching ones
|
||||
containers, errs := m.containerService.ListAllContainers(nil)
|
||||
for _, err := range errs {
|
||||
log.Warn().Err(err).Msg("error listing containers for notifications")
|
||||
}
|
||||
|
||||
// Check each container and start streaming only if it matches any subscription
|
||||
for _, c := range containers {
|
||||
if c.State == "running" {
|
||||
matchingSubs := m.getMatchingSubscriptions(&c)
|
||||
if len(matchingSubs) > 0 {
|
||||
m.startContainerStream(ctx, &c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startContainerStream starts a single log stream for a container
|
||||
// All matching subscriptions will receive logs from this one stream
|
||||
func (m *Manager) startContainerStream(ctx context.Context, c *container.Container) {
|
||||
// Check if already streaming
|
||||
m.mu.RLock()
|
||||
_, exists := m.activeStreams[c.ID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Find which subscriptions match this container
|
||||
matchingSubs := m.getMatchingSubscriptions(c)
|
||||
if len(matchingSubs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("container", c.Name).
|
||||
Int("subscriptions", len(matchingSubs)).
|
||||
Msg("starting log stream for notification subscriptions")
|
||||
|
||||
// Convert to notification.Container and cache it
|
||||
notifContainer := NewContainer(c)
|
||||
m.mu.Lock()
|
||||
m.containerCache[c.ID] = notifContainer
|
||||
m.mu.Unlock()
|
||||
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
stream := &activeStream{
|
||||
container: notifContainer,
|
||||
subscriptions: matchingSubs,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.activeStreams[c.ID] = stream
|
||||
m.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
delete(m.activeStreams, c.ID)
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
|
||||
logs := make(chan *container.LogEvent)
|
||||
|
||||
// Stream logs in separate goroutine
|
||||
go func() {
|
||||
containerService, err := m.containerService.FindContainer(c.Host, c.ID, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error finding container for notification streaming")
|
||||
return
|
||||
}
|
||||
|
||||
err = containerService.StreamLogs(streamCtx, time.Now(), container.STDOUT|container.STDERR, logs)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.Error().Err(err).Msg("error streaming logs for notification")
|
||||
}
|
||||
close(logs)
|
||||
}()
|
||||
|
||||
// Process logs - check against all matching subscriptions
|
||||
for logEvent := range logs {
|
||||
for _, cs := range matchingSubs {
|
||||
m.processLogEvent(logEvent, c.ID, cs)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// getMatchingSubscriptions returns subscriptions that match a container
|
||||
func (m *Manager) getMatchingSubscriptions(c *container.Container) []*compiledSubscription {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
containerCtx := NewContainer(c)
|
||||
matching := make([]*compiledSubscription, 0)
|
||||
|
||||
for _, cs := range m.subscriptions {
|
||||
if m.matchesContainer(cs.containerProgram, containerCtx) {
|
||||
matching = append(matching, cs)
|
||||
}
|
||||
}
|
||||
|
||||
return matching
|
||||
}
|
||||
|
||||
// matchesContainer evaluates the container filter expression
|
||||
func (m *Manager) matchesContainer(program *vm.Program, containerCtx Container) bool {
|
||||
result, err := expr.Run(program, containerCtx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error evaluating container filter")
|
||||
return false
|
||||
}
|
||||
|
||||
match, ok := result.(bool)
|
||||
if !ok {
|
||||
log.Error().Msg("container filter did not return boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// processLogEvent evaluates a log event against the subscription's log filter
|
||||
func (m *Manager) processLogEvent(logEvent *container.LogEvent, containerID string, cs *compiledSubscription) {
|
||||
// Lookup notification.Container from cache
|
||||
m.mu.RLock()
|
||||
notifContainer, ok := m.containerCache[containerID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
log.Warn().Str("container", containerID).Msg("container not in cache")
|
||||
return
|
||||
}
|
||||
|
||||
logCtx := NewLog(logEvent, notifContainer)
|
||||
|
||||
// Evaluate log filter (program is never nil)
|
||||
if m.matchesLog(cs.logProgram, logCtx) {
|
||||
m.triggerWebhook(cs.subscription, logCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// matchesLog evaluates the log filter expression
|
||||
func (m *Manager) matchesLog(program *vm.Program, logCtx Log) bool {
|
||||
result, err := expr.Run(program, logCtx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error evaluating log filter")
|
||||
return false
|
||||
}
|
||||
|
||||
match, ok := result.(bool)
|
||||
if !ok {
|
||||
log.Error().Msg("log filter did not return boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// triggerWebhook sends the notification
|
||||
func (m *Manager) triggerWebhook(sub *Subscription, logCtx Log) {
|
||||
payload := WebhookPayload{
|
||||
SubscriptionName: sub.Name,
|
||||
Timestamp: time.Now(),
|
||||
Container: logCtx.Container,
|
||||
Log: LogEvent{
|
||||
Message: logCtx.Message,
|
||||
Level: logCtx.Level,
|
||||
Timestamp: logCtx.Timestamp,
|
||||
},
|
||||
}
|
||||
|
||||
m.dispatcher.Dispatch(sub, payload)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the YAML configuration structure
|
||||
type Config struct {
|
||||
Dispatcher DispatcherConfig `yaml:"dispatcher"`
|
||||
Subscriptions []*Subscription `yaml:"subscriptions"`
|
||||
}
|
||||
|
||||
// LoadFromFile loads config and returns a fully configured Manager
|
||||
func LoadFromFile(path string, containerService ContainerService) (*Manager, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // No config file is fine, return nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read notifications config: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse notifications config: %w", err)
|
||||
}
|
||||
|
||||
// Validate dispatcher config
|
||||
if config.Dispatcher.Type == "" {
|
||||
config.Dispatcher.Type = "simple" // Default to simple
|
||||
}
|
||||
if config.Dispatcher.URL == "" {
|
||||
return nil, fmt.Errorf("dispatcher.url is required")
|
||||
}
|
||||
|
||||
// Validate subscriptions
|
||||
for _, sub := range config.Subscriptions {
|
||||
if sub.Name == "" {
|
||||
return nil, fmt.Errorf("subscription missing name")
|
||||
}
|
||||
if sub.ContainerFilter == "" {
|
||||
return nil, fmt.Errorf("subscription %q missing container_filter", sub.Name)
|
||||
}
|
||||
if sub.LogFilter == "" {
|
||||
return nil, fmt.Errorf("subscription %q missing log_filter", sub.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create dispatcher based on type
|
||||
var dispatcher Dispatcher
|
||||
switch config.Dispatcher.Type {
|
||||
case "simple":
|
||||
dispatcher = NewWebhookDispatcher(&config.Dispatcher)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported dispatcher type: %s", config.Dispatcher.Type)
|
||||
}
|
||||
|
||||
// Create and configure manager
|
||||
manager := NewManager(containerService, dispatcher)
|
||||
if err := manager.LoadSubscriptions(config.Subscriptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// SaveToFile saves dispatcher config and subscriptions to a YAML file
|
||||
func SaveToFile(path string, dispatcher *DispatcherConfig, subs []*Subscription) error {
|
||||
config := Config{
|
||||
Dispatcher: *dispatcher,
|
||||
Subscriptions: subs,
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notifications config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write notifications config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
// DispatcherConfig represents global dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
// Type determines the dispatcher type (webhook, dozzle-service, etc.)
|
||||
Type string `yaml:"type"` // "webhook" is default
|
||||
|
||||
// URL is the endpoint for the dispatcher service
|
||||
URL string `yaml:"url"`
|
||||
|
||||
// APIKey for authenticating with the dispatcher service
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
|
||||
// Headers to include in all requests
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// Subscription represents a notification rule
|
||||
type Subscription struct {
|
||||
// Name is a human-readable name for this subscription
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// ContainerFilter is an expr expression to match containers (REQUIRED)
|
||||
// Expression receives a Container object with: Name, Labels, Host, State
|
||||
// Example: Name == "nginx" || Labels["app"] == "web"
|
||||
ContainerFilter string `yaml:"container_filter"`
|
||||
|
||||
// LogFilter is an expr expression to match log events (REQUIRED)
|
||||
// Expression receives a Log object with: Message, Level, Timestamp, Container
|
||||
// Example: Level == "error" || Message contains "panic"
|
||||
LogFilter string `yaml:"log_filter"`
|
||||
}
|
||||
|
||||
// Container represents container information for expr evaluation (internal type)
|
||||
type Container struct {
|
||||
Name string `expr:"Name"`
|
||||
Labels map[string]string `expr:"Labels"`
|
||||
Host string `expr:"Host"`
|
||||
State string `expr:"State"`
|
||||
}
|
||||
|
||||
// Log represents log event information for expr evaluation
|
||||
type Log struct {
|
||||
Message any `expr:"Message"`
|
||||
Level string `expr:"Level"`
|
||||
Timestamp time.Time `expr:"Timestamp"`
|
||||
Container Container `expr:"Container"`
|
||||
}
|
||||
|
||||
// WebhookPayload is the structure sent to webhook endpoints
|
||||
type WebhookPayload struct {
|
||||
SubscriptionName string `json:"subscription_name"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Container Container `json:"container"`
|
||||
Log LogEvent `json:"log"`
|
||||
}
|
||||
|
||||
// LogEvent represents the log in the webhook payload
|
||||
type LogEvent struct {
|
||||
Message any `json:"message"`
|
||||
Level string `json:"level"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NewContainer converts container.Container to notification.Container
|
||||
func NewContainer(c *container.Container) Container {
|
||||
return Container{
|
||||
Name: c.Name,
|
||||
Labels: c.Labels,
|
||||
Host: c.Host,
|
||||
State: c.State,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLog creates a Log from container.LogEvent and notification.Container
|
||||
func NewLog(logEvent *container.LogEvent, notifContainer Container) Log {
|
||||
timestamp := time.Unix(0, logEvent.Timestamp)
|
||||
|
||||
// Extract message - keep as any type for expr flexibility
|
||||
var message any = logEvent.Message
|
||||
|
||||
// For grouped logs, join fragments into single string for easier matching
|
||||
if fragments, ok := logEvent.Message.([]container.LogFragment); ok {
|
||||
parts := make([]string, len(fragments))
|
||||
for i, frag := range fragments {
|
||||
parts[i] = frag.Message
|
||||
}
|
||||
message = strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
return Log{
|
||||
Message: message,
|
||||
Level: logEvent.Level,
|
||||
Timestamp: timestamp,
|
||||
Container: notifContainer,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Dispatcher is the interface for sending notifications
|
||||
type Dispatcher interface {
|
||||
Dispatch(subscription *Subscription, payload WebhookPayload)
|
||||
}
|
||||
|
||||
// WebhookDispatcher handles async webhook delivery
|
||||
type WebhookDispatcher struct {
|
||||
client *http.Client
|
||||
config *DispatcherConfig
|
||||
}
|
||||
|
||||
// NewWebhookDispatcher creates a new webhook dispatcher
|
||||
func NewWebhookDispatcher(config *DispatcherConfig) *WebhookDispatcher {
|
||||
return &WebhookDispatcher{
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch sends a webhook payload asynchronously
|
||||
func (d *WebhookDispatcher) Dispatch(subscription *Subscription, payload WebhookPayload) {
|
||||
url := d.config.URL
|
||||
go func() {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to marshal webhook payload")
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to create webhook request")
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Dozzle-Notifications/1.0")
|
||||
|
||||
// Add API key if configured
|
||||
if d.config.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+d.config.APIKey)
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
for key, value := range d.config.Headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to send webhook")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
log.Warn().
|
||||
Int("status", resp.StatusCode).
|
||||
Str("url", url).
|
||||
Str("subscription", payload.SubscriptionName).
|
||||
Msg("webhook returned error status")
|
||||
} else {
|
||||
log.Debug().
|
||||
Int("status", resp.StatusCode).
|
||||
Str("url", url).
|
||||
Str("subscription", payload.SubscriptionName).
|
||||
Msg("webhook delivered successfully")
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -122,6 +122,12 @@ func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events c
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
for _, client := range m.manager.List() {
|
||||
client.SubscribeEvents(ctx, events)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
for _, client := range m.manager.List() {
|
||||
|
||||
@@ -96,6 +96,10 @@ func (m *K8sClusterService) SubscribeEventsAndStats(ctx context.Context, events
|
||||
m.client.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
m.client.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
m.client.SubscribeContainersStarted(ctx, newContainers)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -62,6 +62,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
config["authProvider"] = h.config.Authorization.Provider
|
||||
config["version"] = h.config.Version
|
||||
config["hostname"] = h.config.Hostname
|
||||
config["mode"] = h.config.Mode
|
||||
config["hosts"] = hosts
|
||||
config["disableAvatars"] = h.config.DisableAvatars
|
||||
config["releaseCheckMode"] = h.config.ReleaseCheckMode
|
||||
|
||||
@@ -227,10 +227,34 @@ func (h *handler) streamLogsMerged(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) streamServiceLogs(w http.ResponseWriter, r *http.Request) {
|
||||
service := chi.URLParam(r, "service")
|
||||
func (h *handler) streamLogsWithLabels(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse label filters from URL path
|
||||
// Expected format: /labels/key1:value1,key2:value2/logs/stream
|
||||
labelsParam := chi.URLParam(r, "labels")
|
||||
labelFilters := make(map[string]string)
|
||||
|
||||
if labelsParam != "" {
|
||||
for _, pair := range strings.Split(labelsParam, ",") {
|
||||
parts := strings.SplitN(pair, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
labelFilters[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.streamLogsForContainers(w, r, func(container *container.Container) bool {
|
||||
return container.State == "running" && container.Labels["com.docker.swarm.service.name"] == service
|
||||
if container.State != "running" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if all label filters match
|
||||
for key, value := range labelFilters {
|
||||
if container.Labels[key] != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return len(labelFilters) > 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,14 +266,6 @@ func (h *handler) streamGroupedLogs(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) streamStackLogs(w http.ResponseWriter, r *http.Request) {
|
||||
stack := chi.URLParam(r, "stack")
|
||||
|
||||
h.streamLogsForContainers(w, r, func(container *container.Container) bool {
|
||||
return container.State == "running" && container.Labels["com.docker.stack.namespace"] == stack
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) streamHostLogs(w http.ResponseWriter, r *http.Request) {
|
||||
host := hostKey(r)
|
||||
h.streamLogsForContainers(w, r, func(container *container.Container) bool {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -34,18 +35,20 @@ const (
|
||||
|
||||
// Config is a struct for configuring the web service
|
||||
type Config struct {
|
||||
Base string
|
||||
Addr string
|
||||
Version string
|
||||
Hostname string
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
Authorization Authorization
|
||||
EnableActions bool
|
||||
EnableShell bool
|
||||
DisableAvatars bool
|
||||
ReleaseCheckMode ReleaseCheckMode
|
||||
Labels container.ContainerLabels
|
||||
Base string
|
||||
Addr string
|
||||
Version string
|
||||
Hostname string
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
Mode string
|
||||
Authorization Authorization
|
||||
EnableActions bool
|
||||
EnableShell bool
|
||||
DisableAvatars bool
|
||||
ReleaseCheckMode ReleaseCheckMode
|
||||
Labels container.ContainerLabels
|
||||
NotificationManager *notification.Manager
|
||||
}
|
||||
|
||||
type Authorization struct {
|
||||
@@ -120,8 +123,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates)
|
||||
r.Get("/hosts/{host}/logs/mergedStream/{ids}", h.streamLogsMerged)
|
||||
r.Get("/containers/{hostIds}/download", h.downloadLogs) // formatted as host:container,host:container
|
||||
r.Get("/stacks/{stack}/logs/stream", h.streamStackLogs)
|
||||
r.Get("/services/{service}/logs/stream", h.streamServiceLogs)
|
||||
r.Get("/labels/{labels}/logs/stream", h.streamLogsWithLabels)
|
||||
r.Get("/groups/{group}/logs/stream", h.streamGroupedLogs)
|
||||
r.Get("/events/stream", h.streamEvents)
|
||||
if h.config.EnableActions {
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Containere
|
||||
container: Ingen containere | 1 container | {count} containere
|
||||
host-count: Ingen Værter | 1 Vært | {count} Værter
|
||||
service: Ingen tjenester | 1 tjeneste | {count} tjenester
|
||||
services: Tjenester
|
||||
running-containers: Kørende Containere
|
||||
all-containers: Alle Containere
|
||||
all-namespaces: Alle Navneområder
|
||||
host: Vært
|
||||
hosts: Værter
|
||||
password: Kodeord
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Søg containere (⌘ + k, ⌃k)
|
||||
pin-column: Fastgør som kolonne
|
||||
merge-services: Sammenlæg alle tjenester i én visning
|
||||
merge-containers: Sammenlæg alle containere i én visning
|
||||
merge-hosts: Sammenlæg alle containere på denne vært i én visning
|
||||
merge-all: Sammenlæg alt i én stream
|
||||
error:
|
||||
page-not-found: Denne side eksisterer ikke
|
||||
invalid-auth: Brugernavn eller kodeord er ikke gyldig
|
||||
copy-not-supported: Kopiering til udklipsholder understøttes ikke i din browser
|
||||
logs-skipped: Vis {total} skjulte indtastninger
|
||||
container-not-found: Container ikke fundet
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Visning
|
||||
locale: Overskriv sprog
|
||||
small-scrollbars: Brug mindre scrollbarer
|
||||
show-timesamps: Vis tids stempler
|
||||
show-timestamps: Vis tids stempler
|
||||
soft-wrap: Blødt bræk på linjer
|
||||
datetime-format: Overskriv dato og tids format
|
||||
font-size: Tekststørrelse at bruge til logs
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Container
|
||||
container: Keine Container | 1 Container | {count} Container
|
||||
host-count: Keine Hosts | 1 Host | {count} Hosts
|
||||
service: Kein Service | 1 Service | {count} Services
|
||||
services: Service
|
||||
running-containers: Laufende Container
|
||||
all-containers: Alle Container
|
||||
all-namespaces: Alle Namespaces
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Passwort
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Suche Container (⌘ + k, ⌃k)
|
||||
pin-column: Als Spalte anheften
|
||||
merge-services: Services zusammenführen
|
||||
merge-containers: Container zusammenführen
|
||||
merge-hosts: Alle Container auf diesem Host in einer Ansicht zusammenführen
|
||||
merge-all: Alles in einen Stream zusammenführen
|
||||
error:
|
||||
page-not-found: Diese Seite existiert nicht.
|
||||
invalid-auth: Benutzername und Passwort sind ungültig.
|
||||
copy-not-supported: Kopieren in die Zwischenablage wird von Ihrem Browser nicht unterstützt
|
||||
logs-skipped: Zeige {total} versteckte Einträge
|
||||
container-not-found: Container nicht gefunden.
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Anzeige
|
||||
locale: Sprache überschreiben
|
||||
small-scrollbars: Verwende kleinere Scrollbars
|
||||
show-timesamps: Zeige Zeitstempel
|
||||
show-timestamps: Zeige Zeitstempel
|
||||
soft-wrap: Zeilenumbruch
|
||||
datetime-format: Datums- und Zeitformat
|
||||
font-size: Schriftgröße für Logs
|
||||
|
||||
@@ -20,10 +20,13 @@ action:
|
||||
label:
|
||||
containers: Containers
|
||||
container: No containers | 1 container | {count} containers
|
||||
host-count: No Hosts | 1 Host | {count} Hosts
|
||||
service: No services | 1 service | {count} services
|
||||
services: Services
|
||||
running-containers: Running Containers
|
||||
all-containers: All Containers
|
||||
all-namespaces: All
|
||||
namespaces: Namespaces
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Password
|
||||
@@ -37,6 +40,7 @@ label:
|
||||
per-page: Rows per page
|
||||
host-menu: Hosts and Containers
|
||||
swarm-menu: Services and Stacks
|
||||
k8s-menu: Kubernetes
|
||||
group-menu: Custom Groups
|
||||
no-logs: Container has no logs yet
|
||||
show-all-containers: Show all containers
|
||||
@@ -44,12 +48,11 @@ label:
|
||||
tooltip:
|
||||
search: Search containers (⌘ + k, ⌃k)
|
||||
pin-column: Pin as column
|
||||
merge-services: Merge all services into one view
|
||||
merge-containers: Merge all containers into one view
|
||||
merge-hosts: Merge all containers on this host into one view
|
||||
merge-all: Merge all into one stream
|
||||
error:
|
||||
page-not-found: This page does not exist
|
||||
invalid-auth: Username or password are not valid
|
||||
copy-not-supported: Copy to clipboard is not supported in your browser
|
||||
logs-skipped: Show {total} hidden entries
|
||||
container-not-found: Container not found
|
||||
events-stream:
|
||||
@@ -93,7 +96,7 @@ settings:
|
||||
display: Display
|
||||
locale: Override language
|
||||
small-scrollbars: Use smaller scrollbars
|
||||
show-timesamps: Show timestamps
|
||||
show-timestamps: Show timestamps
|
||||
soft-wrap: Soft wrap lines
|
||||
datetime-format: Override date and time format
|
||||
font-size: Font size to use for logs
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Contenedores
|
||||
container: No contenedores | 1 contenedor | {count} contenedores
|
||||
host-count: Sin Hosts | 1 Host | {count} Hosts
|
||||
service: Sin servicios | 1 servicio | {count} servicios
|
||||
services: Servicios
|
||||
running-containers: Contenedores en ejecución
|
||||
all-containers: Todos los contenedores
|
||||
all-namespaces: Todos los espacios de nombres
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Contraseña
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Buscar contenedores (⌘ + K, CTRL + K)
|
||||
pin-column: Anclar como columna
|
||||
merge-services: Fusionar todos los servicios en una vista
|
||||
merge-containers: Fusionar todos los contenedores en una vista
|
||||
merge-hosts: Fusionar todos los contenedores de este host en una vista
|
||||
merge-all: Fusionar todo en un flujo
|
||||
error:
|
||||
page-not-found: Esta página no existe.
|
||||
invalid-auth: El nombre de usuario y la contraseña no son válidos.
|
||||
copy-not-supported: Copiar al portapapeles no está soportado en su navegador
|
||||
logs-skipped: Mostrar {total} entradas ocultas
|
||||
container-not-found: Contenedor no encontrado.
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Vista
|
||||
locale: Sobrescribir idioma
|
||||
small-scrollbars: Utilizar barras de desplazamiento más pequeñas
|
||||
show-timesamps: Mostrar marcas de tiempo
|
||||
show-timestamps: Mostrar marcas de tiempo
|
||||
soft-wrap: Líneas de texto con ajuste suave
|
||||
datetime-format: Sobrescribir el formato de fecha y hora
|
||||
font-size: Tamaño de letra a utilizar para los registros
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Conteneurs
|
||||
container: Pas de conteneur | 1 conteneur | {count} conteneurs
|
||||
host-count: Aucun Hôte | 1 Hôte | {count} Hôtes
|
||||
service: Pas de service | 1 service | {count} services
|
||||
services: Services
|
||||
running-containers: Conteneurs en execution
|
||||
all-containers: Tous les conteneurs
|
||||
all-namespaces: Tous les espaces de noms
|
||||
host: Hôte
|
||||
hosts: Hôtes
|
||||
password: Mot de passe
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Recherche de conteneurs (⌘ + k, ⌃k)
|
||||
pin-column: Epinglé en colonne
|
||||
merge-services: Fusionner tous les services dans une vue
|
||||
merge-containers: Fusionner tous les conteneurs dans une vue
|
||||
merge-hosts: Fusionner tous les conteneurs de cet hôte dans une vue
|
||||
merge-all: Fusionner tout dans un flux
|
||||
error:
|
||||
page-not-found: Cette page n'existe pas
|
||||
invalid-auth: Nom d'utilisateur ou mot de passe non valides
|
||||
copy-not-supported: La copie dans le presse-papiers n'est pas prise en charge par votre navigateur
|
||||
logs-skipped: Afficher {total} entrées cachées
|
||||
container-not-found: Conteneur non trouvé
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Afficher
|
||||
locale: Langue de remplacement
|
||||
small-scrollbars: Utiliser des barres de défilement plus petites
|
||||
show-timesamps: Afficher les horodatages
|
||||
show-timestamps: Afficher les horodatages
|
||||
soft-wrap: Lignes d'enroulement souples
|
||||
datetime-format: Remplacer le format de la date et de l'heure
|
||||
font-size: Taille de police à utiliser pour les journaux
|
||||
|
||||
@@ -21,10 +21,12 @@ action:
|
||||
label:
|
||||
containers: Kontainer
|
||||
container: Tidak ada kontainer | 1 kontainer | {count} kontainer
|
||||
host-count: Tidak ada Host | 1 Host | {count} Host
|
||||
service: Tidak ada layanan | 1 layanan | {count} layanan
|
||||
services: Layanan
|
||||
running-containers: Kontainer Berjalan
|
||||
all-containers: Semua Kontainer
|
||||
all-namespaces: Semua Namespace
|
||||
host: Host
|
||||
hosts: Host
|
||||
password: Kata sandi
|
||||
@@ -46,13 +48,12 @@ label:
|
||||
tooltip:
|
||||
search: Cari kontainer (⌘ + k, ⌃k)
|
||||
pin-column: Sematkan sebagai kolom
|
||||
merge-services: Gabungkan semua layanan dalam satu tampilan
|
||||
merge-containers: Gabungkan semua kontainer dalam satu tampilan
|
||||
merge-hosts: Gabungkan semua kontainer di host ini dalam satu tampilan
|
||||
merge-all: Gabungkan semua ke dalam satu aliran
|
||||
|
||||
error:
|
||||
page-not-found: Halaman ini tidak ada
|
||||
invalid-auth: Nama pengguna atau kata sandi tidak valid
|
||||
copy-not-supported: Salin ke clipboard tidak didukung di browser Anda
|
||||
logs-skipped: Tampilkan {total} entri tersembunyi
|
||||
container-not-found: Kontainer tidak ditemukan
|
||||
events-stream:
|
||||
@@ -96,7 +97,7 @@ settings:
|
||||
display: Tampilan
|
||||
locale: Ganti bahasa
|
||||
small-scrollbars: Gunakan scrollbar kecil
|
||||
show-timesamps: Tampilkan cap waktu
|
||||
show-timestamps: Tampilkan cap waktu
|
||||
soft-wrap: Bungkus baris secara lunak
|
||||
datetime-format: Ganti format tanggal dan waktu
|
||||
font-size: Ukuran font untuk log
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Container
|
||||
container: Nessun container | 1 container | {count} container
|
||||
host-count: Nessun Host | 1 Host | {count} Host
|
||||
service: Nessun servizio | 1 servizio | {count} servizi
|
||||
services: Servizi
|
||||
running-containers: Container in esecuzione
|
||||
all-containers: Tutti i Container
|
||||
all-namespaces: Tutti i Namespace
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Password
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Ricerca container (⌘ + k, ⌃k)
|
||||
pin-column: Blocca come colonna
|
||||
merge-services: Unisci tutti i servizi in una vista
|
||||
merge-containers: Unisci tutti i container in una vista
|
||||
merge-hosts: Unisci tutti i container su questo host in una vista
|
||||
merge-all: Unisci tutto in un flusso
|
||||
error:
|
||||
page-not-found: Questa pagina non esiste
|
||||
invalid-auth: Username o password non valide
|
||||
copy-not-supported: La copia negli appunti non è supportata dal tuo browser
|
||||
logs-skipped: Mostra {total} voci nascoste
|
||||
container-not-found: Container non trovato
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Visualizza
|
||||
locale: Sovrascrivi Lingua
|
||||
small-scrollbars: Usa una scrollbars più piccola
|
||||
show-timesamps: Visualizza timestamp
|
||||
show-timestamps: Visualizza timestamp
|
||||
soft-wrap: Soft Wrap
|
||||
datetime-format: Sovrascrivi data e formato dell'ora
|
||||
font-size: Grandezza del Font da usare nei log
|
||||
|
||||
@@ -20,6 +20,7 @@ action:
|
||||
label:
|
||||
containers: 컨테이너
|
||||
container: 컨테이너가 없습니다 | 컨테이너 1개 | 컨테이너 {count}개
|
||||
host-count: 호스트가 없습니다 | 호스트 1개 | 호스트 {count}개
|
||||
service: 서비스가 없습니다 | 서비스 1개 | 서비스 {count}개
|
||||
services: 서비스
|
||||
running-containers: 실행 중인 컨테이너
|
||||
@@ -44,12 +45,11 @@ label:
|
||||
tooltip:
|
||||
search: 컨테이너 검색 (⌘ + k, ⌃k)
|
||||
pin-column: 열로 고정
|
||||
merge-services: 모든 서비스를 하나의 화면으로 합치기
|
||||
merge-containers: 모든 컨테이너를 하나의 화면으로 합치기
|
||||
merge-hosts: 이 호스트의 모든 컨테이너를 하나의 화면으로 합치기
|
||||
merge-all: 모두 하나의 스트림으로 병합
|
||||
error:
|
||||
page-not-found: 페이지를 찾을 수 없습니다
|
||||
invalid-auth: 사용자 이름 또는 비밀번호가 올바르지 않습니다
|
||||
copy-not-supported: 클립보드 복사는 브라우저에서 지원되지 않습니다
|
||||
logs-skipped: 숨겨진 항목 {total}개 보기
|
||||
container-not-found: 컨테이너를 찾을 수 없습니다
|
||||
events-stream:
|
||||
@@ -91,7 +91,7 @@ settings:
|
||||
display: 화면 설정
|
||||
locale: 언어 변경
|
||||
small-scrollbars: 작은 스크롤바 사용
|
||||
show-timesamps: 타임스탬프 표시
|
||||
show-timestamps: 타임스탬프 표시
|
||||
soft-wrap: 자동 줄바꿈
|
||||
datetime-format: 날짜 및 시간 형식 변경
|
||||
font-size: 로그 글꼴 크기
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Containers
|
||||
container: Geen containers | 1 container | {count} containers
|
||||
host-count: Geen Hosts | 1 Host | {count} Hosts
|
||||
service: Geen services | 1 service | {count} services
|
||||
services: Services
|
||||
running-containers: Actieve containers
|
||||
all-containers: Alle containers
|
||||
all-namespaces: Alle Namespaces
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Wachtwoord
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Zoek containers (⌘ + k, ⌃k)
|
||||
pin-column: Vastzetten als kolom
|
||||
merge-services: Voeg alle services samen in één weergave
|
||||
merge-containers: Voeg alle containers samen in één weergave
|
||||
merge-hosts: Voeg alle containers op deze host samen in één weergave
|
||||
merge-all: Voeg alles samen in één stream
|
||||
error:
|
||||
page-not-found: Deze pagina bestaat niet
|
||||
invalid-auth: Gebruikersnaam of wachtwoord is ongeldig
|
||||
copy-not-supported: Kopiëren naar klembord wordt niet ondersteund in je browser
|
||||
logs-skipped: Toon {total} verborgen items
|
||||
container-not-found: Container niet gevonden
|
||||
events-stream:
|
||||
@@ -89,7 +90,7 @@ settings:
|
||||
display: Weergave
|
||||
locale: Taal aanpassen
|
||||
small-scrollbars: Kleinere scrollbalk gebruiken
|
||||
show-timesamps: Tijdstempel tonen
|
||||
show-timestamps: Tijdstempel tonen
|
||||
soft-wrap: Regels zacht afbreken
|
||||
datetime-format: Datum- en tijdnotatie aanpassen
|
||||
font-size: Lettergrootte voor logs
|
||||
|
||||
@@ -19,6 +19,7 @@ action:
|
||||
show-details: Pokaż szczegóły
|
||||
label:
|
||||
service: Brak usług | 1 usługa | {count} usług
|
||||
host-count: Brak Hostów | 1 Host | {count} Hostów
|
||||
total-containers: Całkowita liczba kontenerów
|
||||
running: Uruchomione
|
||||
total-cpu-usage: Całkowite użycie CPU
|
||||
@@ -28,6 +29,7 @@ label:
|
||||
container: Brak kontenerów | 1 kontener | {count} kontenerów
|
||||
running-containers: Działające kontenery
|
||||
all-containers: Wszystkie kontenery
|
||||
all-namespaces: Wszystkie przestrzenie nazw
|
||||
host: Host
|
||||
hosts: Hosty
|
||||
password: Hasło
|
||||
@@ -48,12 +50,11 @@ label:
|
||||
tooltip:
|
||||
search: Przeszukaj kontenery (⌘ + k, ⌃k)
|
||||
pin-column: Przypnij jako kolumna
|
||||
merge-services: Scal wszystkie usługi w jeden widok
|
||||
merge-containers: Scal wszystkie kontenery w jeden widok
|
||||
merge-hosts: Scal wszystkie kontenery na tym hoście w jeden widok
|
||||
merge-all: Scal wszystko w jeden strumień
|
||||
error:
|
||||
page-not-found: Ta strona nie istnieje
|
||||
invalid-auth: Nazwa użytkownika lub hasło są niepoprawne
|
||||
copy-not-supported: Kopiowanie do schowka nie jest obsługiwane w twojej przeglądarce
|
||||
logs-skipped: Pokaż {total} ukrytych wpisów
|
||||
container-not-found: Kontener nie został znaleziony
|
||||
events-stream:
|
||||
@@ -94,7 +95,7 @@ settings:
|
||||
display: Wyświetlanie
|
||||
locale: Nadpisz język
|
||||
small-scrollbars: Użyj mniejszych suwaków
|
||||
show-timesamps: Pokaż date i godzinę
|
||||
show-timestamps: Pokaż date i godzinę
|
||||
soft-wrap: Zwijaj linie
|
||||
datetime-format: Nadpisz format daty i godziny
|
||||
font-size: Rozmiar czcionki używany dla logów
|
||||
|
||||
@@ -20,6 +20,7 @@ action:
|
||||
label:
|
||||
containers: Contentores
|
||||
container: Nenhum contentor | 1 contentor | {count} contentores
|
||||
host-count: Nenhum Host | 1 Host | {count} Hosts
|
||||
service: Nenhum serviço | 1 serviço | {count} serviços
|
||||
services: Serviços
|
||||
running-containers: Contentores em execução
|
||||
@@ -48,12 +49,11 @@ label:
|
||||
tooltip:
|
||||
search: Pesquisar contentores (⌘ + K, CTRL + K)
|
||||
pin-column: Alfinete como coluna
|
||||
merge-services: Fundir todos os serviços numa vista
|
||||
merge-containers: Fundir todos os contentores numa vista
|
||||
merge-hosts: Fundir todos os contentores neste anfitrião numa vista
|
||||
merge-all: Mesclar tudo em um fluxo
|
||||
error:
|
||||
page-not-found: Esta página não existe.
|
||||
invalid-auth: O nome de usuário e a senha não são válidos.
|
||||
copy-not-supported: Copiar para a área de transferência não é suportado no seu navegador
|
||||
logs-skipped: Mostrar {total} entradas ocultas
|
||||
container-not-found: Contentor não encontrado.
|
||||
events-stream:
|
||||
@@ -93,7 +93,7 @@ settings:
|
||||
display: Visão
|
||||
locale: Localidade
|
||||
small-scrollbars: Usar barras de rolagem mais pequenas
|
||||
show-timesamps: Mostrar carimbos de tempo
|
||||
show-timestamps: Mostrar carimbos de tempo
|
||||
soft-wrap: Linhas de texto de embrulho suave
|
||||
12-24-format: >-
|
||||
Por defeito, Dozzle utilizará o locale do seu navegador para formatar a hora. Pode
|
||||
|
||||
@@ -20,6 +20,7 @@ action:
|
||||
label:
|
||||
containers: Contêineres
|
||||
container: Nenhum container | 1 container | {count} containers
|
||||
host-count: Nenhum Host | 1 Host | {count} Hosts
|
||||
service: Nenhum serviço | 1 serviço | {count} serviços
|
||||
services: Serviços
|
||||
running-containers: Containers em Execução
|
||||
@@ -44,12 +45,11 @@ label:
|
||||
tooltip:
|
||||
search: Pesquisar containers (⌘ + k, ⌃k)
|
||||
pin-column: Fixar como coluna
|
||||
merge-services: Mesclar todos os serviços em uma visualização
|
||||
merge-containers: Mesclar todos os containers em uma visualização
|
||||
merge-hosts: Mesclar todos os containers neste host em uma visualização
|
||||
merge-all: Unir tudo em um fluxo
|
||||
error:
|
||||
page-not-found: Esta página não existe
|
||||
invalid-auth: Usuário ou senha inválidos
|
||||
copy-not-supported: Copiar para a área de transferência não é suportado no seu navegador
|
||||
logs-skipped: Mostrar {total} entradas ocultas
|
||||
container-not-found: Container não encontrado
|
||||
events-stream:
|
||||
@@ -88,7 +88,7 @@ settings:
|
||||
display: Exibição
|
||||
locale: Sobrescrever idioma
|
||||
small-scrollbars: Usar barras de rolagem menores
|
||||
show-timesamps: Mostrar marcas de tempo
|
||||
show-timestamps: Mostrar marcas de tempo
|
||||
soft-wrap: Quebrar linhas automaticamente
|
||||
datetime-format: Sobrescrever formato de data e hora
|
||||
font-size: Tamanho da fonte para logs
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: Контейнеры
|
||||
container: Нет контейнеров | 1 контейнер | {count} контейнеров
|
||||
host-count: Нет Хостов | 1 Хост | {count} Хостов
|
||||
service: Нет сервисов | 1 сервис | {count} сервисов
|
||||
services: Сервисы
|
||||
running-containers: Запущенные контейнеры
|
||||
all-containers: Все контейнеры
|
||||
all-namespaces: Все пространства имён
|
||||
host: Хост
|
||||
hosts: Хосты
|
||||
password: Пароль
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
pin-column: Закрепить столбец
|
||||
merge-services: Объединить все сервисы в один вид
|
||||
merge-containers: Объединить все контейнеры в один вид
|
||||
merge-hosts: Объединить все контейнеры на этом хосте в один вид
|
||||
merge-all: Объединить все в один поток
|
||||
error:
|
||||
page-not-found: Эта страница не доступна.
|
||||
invalid-auth: Имя пользователя или пароль неверны.
|
||||
copy-not-supported: Копирование в буфер обмена не поддерживается вашим браузером
|
||||
logs-skipped: Показать {total} скрытых записей
|
||||
container-not-found: Контейнер не найден.
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: Вид
|
||||
locale: Язык
|
||||
small-scrollbars: Уменьшенная полоса прокрутки
|
||||
show-timesamps: Показывать временные метки
|
||||
show-timestamps: Показывать временные метки
|
||||
soft-wrap: Плавный перенос текста
|
||||
datetime-format: Формат даты и времени
|
||||
font-size: Размер шрифта
|
||||
|
||||
@@ -19,6 +19,7 @@ action:
|
||||
show-details: Prikaži podrobnosti
|
||||
label:
|
||||
containers: Zabojniki
|
||||
host-count: Ni Gostiteljev | 1 Gostitelj | {count} Gostiteljev
|
||||
service: Ni storitev | 1 storitev | {count} storitev
|
||||
running-containers: Delujoči zabojniki
|
||||
all-containers: Vsi zabojniki
|
||||
@@ -44,12 +45,11 @@ label:
|
||||
tooltip:
|
||||
search: Iskanje zabojnikov (⌘ + k, ⌃k)
|
||||
pin-column: Pripni kot stolpec
|
||||
merge-services: Združite vse storitve v en pogled
|
||||
merge-hosts: Združi vse zabojnike na tem gostitelju v en pogled
|
||||
merge-containers: Združi vse zabojnike v en pogled
|
||||
merge-all: Združi vse v en tok
|
||||
error:
|
||||
page-not-found: Ta stran ne obstaja
|
||||
invalid-auth: Uporabniško ime ali geslo nista veljavna
|
||||
copy-not-supported: Kopiranje v odložišče ni podprto v vašem brskalniku
|
||||
logs-skipped: Prikaži {total} skritih vnosov
|
||||
events-stream:
|
||||
title: Nepričakovana napaka
|
||||
@@ -88,7 +88,7 @@ settings:
|
||||
display: Prikaz
|
||||
locale: Preglasi jezik
|
||||
small-scrollbars: Uporabite manjše drsne trakove
|
||||
show-timesamps: Prikaži časovne žige
|
||||
show-timestamps: Prikaži časovne žige
|
||||
soft-wrap: Mehke ovojne linije
|
||||
datetime-format: Preglasi obliko zapisa datuma in časa
|
||||
font-size: Velikost pisave za dnevnike
|
||||
|
||||
@@ -20,6 +20,7 @@ action:
|
||||
label:
|
||||
containers: Konteynerlar
|
||||
container: Konteyner yok | 1 konteyner | {count} konteyner
|
||||
host-count: Ana Bilgisayar yok | 1 Ana Bilgisayar | {count} Ana Bilgisayar
|
||||
service: Servis yok | 1 servis | {count} servis
|
||||
services: Servisler
|
||||
running-containers: Çalışan Konteynerlar
|
||||
@@ -50,12 +51,11 @@ label:
|
||||
tooltip:
|
||||
search: Konteynerlerde ara (⌘ + k, ⌃k)
|
||||
pin-column: Sütun olarak sabitle
|
||||
merge-services: Tüm servisleri tek görünümde birleştir
|
||||
merge-containers: Tüm konteynerleri tek görünümde birleştir
|
||||
merge-hosts: Bu sunucudaki tüm konteynerleri tek görünümde birleştir
|
||||
merge-all: Tümünü tek bir akışta birleştir
|
||||
error:
|
||||
page-not-found: Bu sayfa bulunamadı
|
||||
invalid-auth: Kullanıcı adı veya şifre geçersiz
|
||||
copy-not-supported: Panoya kopyalama tarayıcınızda desteklenmiyor
|
||||
logs-skipped: "{total} gizli girişi göster"
|
||||
container-not-found: Konteyner bulunamadı
|
||||
events-stream:
|
||||
@@ -97,7 +97,7 @@ settings:
|
||||
display: Görünüm
|
||||
locale: Dili geçersiz kıl
|
||||
small-scrollbars: Daha küçük kaydırma çubukları kullan
|
||||
show-timesamps: Zaman damgalarını göster
|
||||
show-timestamps: Zaman damgalarını göster
|
||||
soft-wrap: Satırları yumuşak kaydır
|
||||
datetime-format: Tarih ve saat biçimini geçersiz kıl
|
||||
font-size: Günlükler için kullanılacak yazı tipi boyutu
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: 容器
|
||||
container: 無容器 | 1 個容器 | {count} 個容器
|
||||
host-count: 無主機 | 1 個主機 | {count} 個主機
|
||||
service: 無服務 | 1 個服務 | {count} 個服務
|
||||
services: 服務
|
||||
running-containers: 運作中的容器
|
||||
all-containers: 所有容器
|
||||
all-namespaces: 所有命名空間
|
||||
no-logs: 容器尚無日誌
|
||||
show-all-containers: 顯示所有容器
|
||||
collapse-all: 摺疊全部
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: 搜尋容器 (⌘ + k, ⌃k)
|
||||
pin-column: 釘選為欄位
|
||||
merge-services: 將所有服務合併至單一檢視
|
||||
merge-containers: 將所有容器合併至單一檢視
|
||||
merge-hosts: 將此主機上的所有容器合併至單一檢視
|
||||
merge-all: 將所有內容合併到一個流中
|
||||
error:
|
||||
page-not-found: 此頁面不存在
|
||||
invalid-auth: 使用者名稱或密碼不正確
|
||||
copy-not-supported: 您的瀏覽器不支援複製到剪貼簿
|
||||
logs-skipped: 顯示 {total} 個隱藏項目
|
||||
container-not-found: 找不到容器
|
||||
events-stream:
|
||||
@@ -90,7 +91,7 @@ settings:
|
||||
display: 顯示
|
||||
locale: 變更語言
|
||||
small-scrollbars: 使用較小的捲軸
|
||||
show-timesamps: 顯示時間戳記
|
||||
show-timestamps: 顯示時間戳記
|
||||
soft-wrap: 自動換行
|
||||
datetime-format: 變更日期與時間格式
|
||||
font-size: 日誌字型大小
|
||||
|
||||
@@ -20,10 +20,12 @@ action:
|
||||
label:
|
||||
containers: 容器
|
||||
container: 无容器 | 1 容器 | {count} 容器
|
||||
host-count: 无主机 | 1 个主机 | {count} 个主机
|
||||
service: 无服务 | 1 个服务 | {count} 个服务
|
||||
services: 服务
|
||||
running-containers: 运行中的容器
|
||||
all-containers: 所有容器
|
||||
all-namespaces: 所有命名空间
|
||||
no-logs: 容器尚无日志
|
||||
show-all-containers: 显示所有容器
|
||||
collapse-all: 折叠全部
|
||||
@@ -44,12 +46,11 @@ label:
|
||||
tooltip:
|
||||
search: 搜索 (⌘ + k, ⌃k)
|
||||
pin-column: 固定为列
|
||||
merge-services: 合并所有服务到一个视图
|
||||
merge-containers: 合并所有容器到一个视图
|
||||
merge-hosts: 合并此主机上的所有容器到一个视图
|
||||
merge-all: 将所有内容合并到一个流中
|
||||
error:
|
||||
page-not-found: 此页面不存在。
|
||||
invalid-auth: 用户名和密码无效。
|
||||
copy-not-supported: 您的浏览器不支持复制到剪贴板
|
||||
logs-skipped: 显示 {total} 个隐藏条目
|
||||
container-not-found: 容器未找到。
|
||||
events-stream:
|
||||
@@ -88,7 +89,7 @@ settings:
|
||||
display: 显示
|
||||
locale: 显示语言
|
||||
small-scrollbars: 使用较小的滚动条
|
||||
show-timesamps: 显示时间戳
|
||||
show-timestamps: 显示时间戳
|
||||
soft-wrap: 断行
|
||||
datetime-format: 自定义日期和时间格式
|
||||
font-size: 字体大小
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/k8s"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/support/cli"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
k8s_support "github.com/amir20/dozzle/internal/support/k8s"
|
||||
@@ -108,15 +109,18 @@ func main() {
|
||||
log.Fatal().Str("mode", args.Mode).Msg("Invalid mode")
|
||||
}
|
||||
|
||||
srv := createServer(args, hostService)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
notificationManager := initializeNotifications(ctx, hostService)
|
||||
|
||||
srv := createServer(args, hostService, notificationManager)
|
||||
go func() {
|
||||
log.Info().Msgf("Accepting connections on %s", args.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("failed to listen")
|
||||
}
|
||||
}()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
@@ -137,7 +141,56 @@ func fileExists(filename string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
func initializeSimpleAuth(authTTL string) web.Authorizer {
|
||||
log.Debug().Msg("Using simple authentication")
|
||||
|
||||
userFilePath := "./data/users.yml"
|
||||
if !fileExists(userFilePath) {
|
||||
userFilePath = "./data/users.yaml"
|
||||
if !fileExists(userFilePath) {
|
||||
log.Fatal().Msg("No users.yaml or users.yml file found.")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Reading %s file", filepath.Base(userFilePath))
|
||||
|
||||
db, err := auth.ReadUsersFromFile(userFilePath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("Could not read users file: %s", userFilePath)
|
||||
}
|
||||
|
||||
log.Debug().Int("users", len(db.Users)).Msg("Loaded users")
|
||||
ttl := time.Duration(0)
|
||||
if authTTL != "session" {
|
||||
ttl, err = time.ParseDuration(authTTL)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not parse auth ttl")
|
||||
}
|
||||
}
|
||||
return auth.NewSimpleAuth(db, ttl)
|
||||
}
|
||||
|
||||
func initializeNotifications(ctx context.Context, hostService web.HostService) *notification.Manager {
|
||||
notificationsPath := "./data/notifications.yml"
|
||||
manager, err := notification.LoadFromFile(notificationsPath, hostService)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load notifications config")
|
||||
return nil
|
||||
}
|
||||
|
||||
if manager == nil {
|
||||
return nil // No config file
|
||||
}
|
||||
|
||||
if err := manager.Start(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("failed to start notification manager")
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
func createServer(args cli.Args, hostService web.HostService, notificationManager *notification.Manager) *http.Server {
|
||||
_, dev := os.LookupEnv("DEV")
|
||||
|
||||
var releaseCheckMode web.ReleaseCheckMode = web.Automatic
|
||||
@@ -158,33 +211,8 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
provider = web.FORWARD_PROXY
|
||||
authorizer = auth.NewForwardProxyAuth(args.AuthHeaderUser, args.AuthHeaderEmail, args.AuthHeaderName, args.AuthHeaderFilter, args.AuthHeaderRoles)
|
||||
} else if args.AuthProvider == "simple" {
|
||||
log.Debug().Msg("Using simple authentication")
|
||||
provider = web.SIMPLE
|
||||
|
||||
userFilePath := "./data/users.yml"
|
||||
if !fileExists(userFilePath) {
|
||||
userFilePath = "./data/users.yaml"
|
||||
if !fileExists(userFilePath) {
|
||||
log.Fatal().Msg("No users.yaml or users.yml file found.")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Reading %s file", filepath.Base(userFilePath))
|
||||
|
||||
db, err := auth.ReadUsersFromFile(userFilePath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("Could not read users file: %s", userFilePath)
|
||||
}
|
||||
|
||||
log.Debug().Int("users", len(db.Users)).Msg("Loaded users")
|
||||
ttl := time.Duration(0)
|
||||
if args.AuthTTL != "session" {
|
||||
ttl, err = time.ParseDuration(args.AuthTTL)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not parse auth ttl")
|
||||
}
|
||||
}
|
||||
authorizer = auth.NewSimpleAuth(db, ttl)
|
||||
authorizer = initializeSimpleAuth(args.AuthTTL)
|
||||
}
|
||||
|
||||
authTTL := time.Duration(0)
|
||||
@@ -204,17 +232,19 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
Hostname: args.Hostname,
|
||||
NoAnalytics: args.NoAnalytics,
|
||||
Dev: dev,
|
||||
Mode: args.Mode,
|
||||
Authorization: web.Authorization{
|
||||
Provider: provider,
|
||||
Authorizer: authorizer,
|
||||
TTL: authTTL,
|
||||
LogoutUrl: args.AuthLogoutUrl,
|
||||
},
|
||||
EnableActions: args.EnableActions,
|
||||
EnableShell: args.EnableShell,
|
||||
DisableAvatars: args.DisableAvatars,
|
||||
ReleaseCheckMode: releaseCheckMode,
|
||||
Labels: args.Filter,
|
||||
EnableActions: args.EnableActions,
|
||||
EnableShell: args.EnableShell,
|
||||
DisableAvatars: args.DisableAvatars,
|
||||
ReleaseCheckMode: releaseCheckMode,
|
||||
Labels: args.Filter,
|
||||
NotificationManager: notificationManager,
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(content, "dist")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "8.14.12",
|
||||
"version": "9.0.1",
|
||||
"description": "Realtime log viewer for docker containers.",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,33 +29,27 @@
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@duckdb/duckdb-wasm": "1.30.0",
|
||||
"@iconify-json/carbon": "^1.2.15",
|
||||
"@duckdb/duckdb-wasm": "1.33.1-dev16.0",
|
||||
"@iconify-json/carbon": "^1.2.16",
|
||||
"@iconify-json/cil": "^1.2.3",
|
||||
"@iconify-json/ic": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify-json/material-symbols": "^1.2.51",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/mdi-light": "^1.2.2",
|
||||
"@iconify-json/octicon": "^1.2.19",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "4.1.18",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/integrations": "^14.1.0",
|
||||
"@vueuse/router": "14.1.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "5.5.13",
|
||||
"daisyui": "5.5.14",
|
||||
"entities": "^7.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@@ -68,47 +62,41 @@
|
||||
"unplugin-icons": "^22.5.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-router": "^0.19.0",
|
||||
"vite": "7.2.7",
|
||||
"unplugin-vue-router": "^0.19.2",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitepress": "1.6.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-arrow/esnext-esm": "^21.1.0",
|
||||
"@iconify-json/ion": "^1.2.6",
|
||||
"@iconify-json/material-symbols-light": "^1.2.50",
|
||||
"@iconify-json/ri": "^1.2.6",
|
||||
"@iconify-json/material-symbols-light": "^1.2.51",
|
||||
"@iconify-json/ri": "^1.2.7",
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/d3-array": "^3.2.2",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/d3-transition": "^3.0.9",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/node": "^25.0.6",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"@vue/compiler-sfc": "^3.5.25",
|
||||
"@vue/compiler-sfc": "^3.5.26",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"bumpp": "^10.3.2",
|
||||
"c8": "^10.1.3",
|
||||
"concurrently": "^9.2.1",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.15",
|
||||
"vue-component-type-helpers": "3.1.8",
|
||||
"vue-tsc": "3.1.8"
|
||||
"vitest": "^4.0.16",
|
||||
"vue-component-type-helpers": "3.2.2",
|
||||
"vue-tsc": "3.2.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css,ts,html,md}": [
|
||||
|
||||