feat: implements web hooks and dispatchers with a complete new notificaiton page (#4349)

This commit is contained in:
Amir Raminfar
2026-01-19 17:30:36 -08:00
committed by GitHub
parent bca061fa7b
commit 321bf43f1c
49 changed files with 12959 additions and 448 deletions
+1
View File
@@ -41,6 +41,7 @@ COPY internal ./internal
COPY types ./types
COPY main.go ./
COPY protos ./protos
COPY graph ./graph
COPY shared_key.pem shared_cert.pem ./
# Copy assets built with node
+9
View File
@@ -27,10 +27,13 @@ declare global {
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createContainerHints: typeof import('./composable/exprEditor').createContainerHints
const createDrawer: typeof import('./composable/drawer').createDrawer
const createEventHook: typeof import('@vueuse/core').createEventHook
const createExprEditor: typeof import('./composable/exprEditor').createExprEditor
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createLogHints: typeof import('./composable/exprEditor').createLogHints
const createPinia: typeof import('pinia').createPinia
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
@@ -397,6 +400,9 @@ declare global {
export type { LogStreamSource } from './composable/eventStreams'
import('./composable/eventStreams')
// @ts-ignore
export type { ExprEditorOptions } from './composable/exprEditor'
import('./composable/exprEditor')
// @ts-ignore
export type { Config, Profile } from './stores/config'
import('./stores/config')
// @ts-ignore
@@ -436,10 +442,13 @@ declare module 'vue' {
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createContainerHints: UnwrapRef<typeof import('./composable/exprEditor')['createContainerHints']>
readonly createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createExprEditor: UnwrapRef<typeof import('./composable/exprEditor')['createExprEditor']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createLogHints: UnwrapRef<typeof import('./composable/exprEditor')['createLogHints']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
+15
View File
@@ -11,6 +11,8 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AlertCard: typeof import('./components/Notification/AlertCard.vue')['default']
AlertForm: typeof import('./components/Notification/AlertForm.vue')['default']
Announcements: typeof import('./components/Announcements.vue')['default']
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
@@ -41,6 +43,8 @@ declare module 'vue' {
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
DateTime: typeof import('./components/common/DateTime.vue')['default']
DestinationCard: typeof import('./components/Notification/DestinationCard.vue')['default']
DestinationForm: typeof import('./components/Notification/DestinationForm.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
@@ -80,15 +84,21 @@ declare module 'vue' {
'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default']
'MaterialSymbolsLight:collapseAll': typeof import('~icons/material-symbols-light/collapse-all')['default']
'Mdi:account': typeof import('~icons/mdi/account')['default']
'Mdi:alert': typeof import('~icons/mdi/alert')['default']
'Mdi:alertOutline': typeof import('~icons/mdi/alert-outline')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
'Mdi:bell': typeof import('~icons/mdi/bell')['default']
'Mdi:bellOutline': typeof import('~icons/mdi/bell-outline')['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:clockOutline': typeof import('~icons/mdi/clock-outline')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cloud': typeof import('~icons/mdi/cloud')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
@@ -99,7 +109,12 @@ declare module 'vue' {
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:packageVariantClosed': typeof import('~icons/mdi/package-variant-closed')['default']
'Mdi:pencilOutline': typeof import('~icons/mdi/pencil-outline')['default']
'Mdi:plus': typeof import('~icons/mdi/plus')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
'Mdi:trashCanOutline': typeof import('~icons/mdi/trash-can-outline')['default']
'Mdi:webhook': typeof import('~icons/mdi/webhook')['default']
MetricCard: typeof import('./components/MetricCard.vue')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
+9
View File
@@ -3,6 +3,15 @@
<slot name="more-items"></slot>
<Announcements />
<router-link
:to="{ name: '/notifications' }"
:aria-label="$t('title.notifications')"
data-testid="notifications"
class="btn btn-circle btn-sm"
>
<mdi:bell class="size-6" />
</router-link>
<router-link
:to="{ name: '/settings' }"
:aria-label="$t('title.settings')"
+2 -2
View File
@@ -14,12 +14,12 @@
</template>
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
import type { LogEntry, LogMessage } from "@/models/LogEntry";
const { progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | string[] | JSONObject>[];
messages: LogEntry<LogMessage>[];
}>();
const { containers } = useLoggingContext();
@@ -0,0 +1,109 @@
<template>
<div class="card bg-base-100 shadow-sm" :class="{ 'opacity-60': !alert.enabled }">
<div class="card-body gap-4 p-5">
<!-- Header -->
<div class="flex items-start justify-between">
<div class="flex items-center gap-2">
<h4 class="text-lg font-semibold">{{ alert.name }}</h4>
<span v-if="!alert.enabled" class="badge badge-warning badge-sm">{{ $t("notifications.alert.paused") }}</span>
</div>
<input type="checkbox" class="toggle toggle-primary" :checked="alert.enabled" @change="toggleEnabled" />
</div>
<!-- Expressions -->
<div class="text-base-content/80 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
<span>{{ $t("notifications.alert.containers") }}</span>
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.containerExpression }}</code>
<span>{{ $t("notifications.alert.log-filter") }}</span>
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.logExpression }}</code>
<span>{{ $t("notifications.alert.destination") }}</span>
<span v-if="alert.dispatcher" class="flex items-center gap-1.5">
<mdi:webhook v-if="alert.dispatcher.type === 'webhook'" />
<mdi:cloud v-else />
{{ alert.dispatcher.name }}
</span>
<span v-else class="text-warning flex items-center gap-1.5">
<mdi:alert-outline />
{{ $t("notifications.alert.dispatcher-deleted") }}
</span>
</div>
<!-- Footer -->
<div class="border-base-content/10 text-base-content/80 flex items-center justify-between border-t pt-3 text-sm">
<div class="flex items-center gap-4">
<span class="flex items-center gap-1">
<mdi:package-variant-closed class="text-base" />
{{ $t("notifications.alert.containers-count", { count: alert.triggeredContainers }) }}
</span>
<span class="flex items-center gap-1">
<mdi:bell-outline class="text-base" />
{{ $t("notifications.alert.triggered-count", { count: alert.triggerCount }) }}
</span>
<span v-if="alert.lastTriggeredAt" class="flex items-center gap-1">
<mdi:clock-outline class="text-base" />
{{ $t("notifications.alert.last-triggered", { time: formatTimeAgo(alert.lastTriggeredAt) }) }}
</span>
</div>
<div class="flex items-center gap-1">
<button class="btn btn-ghost btn-square" @click="editAlert">
<mdi:pencil-outline />
</button>
<button class="btn btn-ghost btn-square text-error" @click="deleteAlert" :disabled="isDeleting">
<span v-if="isDeleting" class="loading loading-spinner loading-xs"></span>
<mdi:trash-can-outline v-else />
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useMutation, useQuery } from "@urql/vue";
import {
GetNotificationRulesDocument,
DeleteNotificationRuleDocument,
UpdateNotificationRuleDocument,
type NotificationRule,
} from "@/types/graphql";
import AlertForm from "./AlertForm.vue";
const { alert } = defineProps<{
alert: NotificationRule;
}>();
const showDrawer = useDrawer();
const deleteMutation = useMutation(DeleteNotificationRuleDocument);
const updateMutation = useMutation(UpdateNotificationRuleDocument);
const alertsQuery = useQuery({ query: GetNotificationRulesDocument, pause: true });
const isDeleting = ref(false);
function formatTimeAgo(dateStr: string): string {
const date = new Date(dateStr);
if (date.getFullYear() === 0) return "-";
return toRelativeTime(date, undefined);
}
async function toggleEnabled() {
await updateMutation.executeMutation({
id: alert.id,
input: { enabled: !alert.enabled },
});
alertsQuery.executeQuery({ requestPolicy: "network-only" });
}
function editAlert() {
showDrawer(AlertForm, { alert, onCreated: () => alertsQuery.executeQuery({ requestPolicy: "network-only" }) }, "lg");
}
async function deleteAlert() {
isDeleting.value = true;
try {
await deleteMutation.executeMutation({ id: alert.id });
alertsQuery.executeQuery({ requestPolicy: "network-only" });
} finally {
isDeleting.value = false;
}
}
</script>
@@ -0,0 +1,353 @@
<template>
<div class="space-y-6 p-4">
<div class="mb-6">
<h2 class="text-2xl font-bold">
{{ isEditing ? $t("notifications.alert-form.edit-title") : $t("notifications.alert-form.create-title") }}
</h2>
<p class="text-base-content/60">{{ $t("notifications.alert-form.description") }}</p>
</div>
<!-- Alert Name -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.alert-name") }}</legend>
<input
ref="alertNameInput"
v-model="alertName"
type="text"
class="input focus:input-primary w-full text-base"
:class="alertName.trim() ? 'input-primary' : ''"
required
:placeholder="$t('notifications.alert-form.alert-name-placeholder')"
/>
</fieldset>
<!-- Container Filter -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.container-filter") }}</legend>
<div
class="input focus-within:input-primary w-full focus-within:z-50"
:class="
containerExpression.trim() && !containerResult?.error
? 'input-primary'
: { 'input-error!': containerResult?.error }
"
>
<div ref="containerEditorRef" class="w-full"></div>
</div>
<div v-if="containerResult" class="fieldset-label">
<span v-if="containerResult.error" class="text-error">{{ containerResult.error }}</span>
<span v-else-if="containerResult.containers?.length" class="text-success">
<mdi:check class="inline" />
{{
$t("notifications.alert-form.containers-match", {
count: containerResult.containers.length,
names: containerResult.containers.map((c) => c.name).join(", "),
})
}}
</span>
<span v-else class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-containers-match") }}
</span>
</div>
</fieldset>
<!-- Log Filter -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.log-filter") }}</legend>
<div
class="input focus-within:input-primary w-full focus-within:z-50"
:class="logExpression.trim() && !logError ? 'input-primary' : { 'input-error!': logError }"
>
<div ref="logEditorRef" class="w-full"></div>
</div>
<div v-if="logError || logExpression" class="fieldset-label">
<span v-if="logError" class="text-error">{{ logError }}</span>
<span v-else-if="logMessages.length" class="text-success">
<mdi:check class="inline" />
{{ $t("notifications.alert-form.logs-match", { count: logTotalCount }) }}
</span>
<span v-else-if="!isLoading" class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-logs-match") }}
</span>
</div>
</fieldset>
<!-- Destination -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.destination") }}</legend>
<details class="dropdown w-full" ref="destinationDropdown">
<summary class="btn btn-outline w-full justify-between" :class="{ 'btn-primary': selectedDestination }">
<span class="flex items-center gap-2">
<template v-if="selectedDestination">
<mdi:webhook v-if="selectedDestination.type === 'webhook'" />
<mdi:cloud v-else />
{{ selectedDestination.name }}
</template>
<span v-else class="text-base-content/60">{{ $t("notifications.alert-form.select-destination") }}</span>
</span>
<carbon:caret-down />
</summary>
<ul class="dropdown-content menu bg-base-200 rounded-box z-50 mt-1 w-full border p-2 shadow-sm">
<li v-for="dest in destinations" :key="dest.id">
<a
@click="
dispatcherId = dest.id;
destinationDropdown?.removeAttribute('open');
"
:class="{ active: dispatcherId === dest.id }"
>
<mdi:webhook v-if="dest.type === 'webhook'" />
<mdi:cloud v-else />
{{ dest.name }}
</a>
</li>
</ul>
</details>
<div v-if="!destinations.length" class="fieldset-label">
<span class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-destinations") }}
</span>
</div>
</fieldset>
<!-- Log Preview -->
<div v-if="logMessages.length" class="mt-4">
<div class="mb-2 text-lg">{{ $t("notifications.alert-form.preview") }}</div>
<LogList
:messages="logMessages"
:last-selected-item="undefined"
class="border-base-content/50 h-64 overflow-hidden rounded-lg border"
/>
</div>
<!-- Error -->
<div v-if="saveError" class="alert alert-error">
<span>{{ saveError }}</span>
</div>
<!-- Actions -->
<div class="flex justify-end gap-2 pt-4">
<button class="btn" @click="close?.()">{{ $t("notifications.alert-form.cancel") }}</button>
<button class="btn btn-primary" :disabled="!canSave" @click="saveAlert">
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
{{ isEditing ? $t("notifications.alert-form.save") : $t("notifications.alert-form.create") }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { type LogEvent, type LogEntry, type LogMessage, asLogEntry } from "@/models/LogEntry";
import { Container } from "@/models/Container";
import type { ContainerJson } from "@/types/Container";
import { createExprEditor, createContainerHints, createLogHints } from "@/composable/exprEditor";
import { useQuery, useMutation } from "@urql/vue";
import {
GetDispatchersDocument,
CreateNotificationRuleDocument,
ReplaceNotificationRuleDocument,
PreviewExpressionDocument,
} from "@/types/graphql";
export interface AlertData {
id: number;
name: string;
containerExpression: string;
logExpression: string;
dispatcherId: number;
}
const { close, onCreated, alert } = defineProps<{
close?: () => void;
onCreated?: () => void;
alert?: AlertData;
}>();
// GraphQL
const dispatchersQuery = useQuery({ query: GetDispatchersDocument });
const createMutation = useMutation(CreateNotificationRuleDocument);
const replaceMutation = useMutation(ReplaceNotificationRuleDocument);
const previewMutation = useMutation(PreviewExpressionDocument);
// Container store for autocomplete hints
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const containerNames = computed(() => [
...new Set(containers.value.filter((c) => c.state === "running").map((c) => c.name)),
]);
const imageNames = computed(() => [...new Set(containers.value.map((c) => c.image))]);
const hostNames = computed(() => [...new Set(containers.value.map((c) => c.host))]);
// Template refs
const alertNameInput = ref<HTMLInputElement>();
const containerEditorRef = ref<HTMLElement>();
const logEditorRef = ref<HTMLElement>();
const destinationDropdown = ref<HTMLDetailsElement>();
// Form state
const isEditing = computed(() => !!alert);
const alertName = ref(alert?.name ?? "");
const containerExpression = ref(alert?.containerExpression ?? "");
const logExpression = ref(alert?.logExpression ?? "");
const dispatcherId = ref(alert?.dispatcherId ?? 0);
const destinations = computed(() => dispatchersQuery.data.value?.dispatchers ?? []);
const selectedDestination = computed(() => destinations.value.find((d) => d.id === dispatcherId.value));
useFocus(alertNameInput, { initialValue: true });
// Validation state
interface ContainerResult {
error?: string;
containers?: Container[];
}
const containerResult = ref<ContainerResult | null>(null);
const logError = ref<string | null>(null);
const logTotalCount = ref(0);
const logMessages = shallowRef<LogEntry<LogMessage>[]>([]);
const isLoading = ref(false);
const isSaving = ref(false);
const saveError = ref<string | null>(null);
const canSave = computed(
() =>
alertName.value.trim() &&
containerExpression.value.trim() &&
dispatcherId.value > 0 &&
!containerResult.value?.error &&
!logError.value &&
!isSaving.value,
);
async function saveAlert() {
if (!canSave.value) return;
isSaving.value = true;
saveError.value = null;
try {
const input = {
name: alertName.value.trim(),
containerExpression: containerExpression.value,
logExpression: logExpression.value,
dispatcherId: dispatcherId.value!,
enabled: true,
};
const result = isEditing.value
? await replaceMutation.executeMutation({ id: alert!.id, input })
: await createMutation.executeMutation({ input });
if (result.error) {
throw new Error(result.error.message);
}
onCreated?.();
close?.();
} catch (e) {
saveError.value = e instanceof Error ? e.message : "Failed to save alert";
} finally {
isSaving.value = false;
}
}
async function validateExpressions() {
if (!containerExpression.value && !logExpression.value) {
containerResult.value = null;
logError.value = null;
logTotalCount.value = 0;
logMessages.value = [];
return;
}
isLoading.value = true;
try {
const result = await previewMutation.executeMutation({
input: {
containerExpression: containerExpression.value,
logExpression: logExpression.value || undefined,
},
});
if (result.error) throw new Error(result.error.message);
const data = result.data?.previewExpression;
if (!data) throw new Error("No data returned");
// Update container result
containerResult.value = containerExpression.value
? {
error: data.containerError ?? undefined,
containers: data.matchedContainers?.map((c) => Container.fromJSON(c as ContainerJson)),
}
: null;
// Update log result
if (logExpression.value && !data.containerError) {
logError.value = data.logError ?? null;
logTotalCount.value = data.totalLogs;
logMessages.value =
data.matchedLogs?.map((event) =>
asLogEntry({
t: (event.type as LogEvent["t"]) ?? "single",
m: event.message as LogEvent["m"],
ts: event.timestamp,
id: event.id,
l: (event.level as LogEvent["l"]) ?? "unknown",
s: (event.stream as LogEvent["s"]) ?? "unknown",
c: "",
rm: "",
}),
) ?? [];
} else {
logError.value = null;
logTotalCount.value = 0;
logMessages.value = [];
}
} catch (e) {
containerResult.value = { error: e instanceof Error ? e.message : "Unknown error" };
} finally {
isLoading.value = false;
}
}
const debouncedValidate = useDebounceFn(validateExpressions, 500);
watch([containerExpression, logExpression], () => {
isLoading.value = true;
debouncedValidate();
});
let containerEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
let logEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
onMounted(async () => {
if (containerEditorRef.value) {
containerEditorView = await createExprEditor({
parent: containerEditorRef.value,
placeholder: 'name contains "api"',
initialValue: alert?.containerExpression ?? "",
getHints: () => createContainerHints(containerNames.value, imageNames.value, hostNames.value),
onChange: (v) => (containerExpression.value = v),
});
}
if (logEditorRef.value) {
logEditorView = await createExprEditor({
parent: logEditorRef.value,
placeholder: 'level == "error" && message contains "timeout"',
initialValue: alert?.logExpression ?? "",
getHints: createLogHints,
onChange: (v) => (logExpression.value = v),
});
}
});
onScopeDispose(() => {
containerEditorView?.destroy();
logEditorView?.destroy();
});
</script>
@@ -0,0 +1,68 @@
<template>
<div class="card bg-base-100">
<div class="card-body gap-2 p-4">
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-lg">
<mdi:webhook v-if="destination.type === 'webhook'" class="text-lg" />
<mdi:cloud v-else class="text-primary-content text-lg" />
</div>
<div class="flex-1">
<h4 class="font-semibold">{{ destination.name }}</h4>
<p class="text-base-content/60 text-sm">
{{
destination.type === "webhook"
? $t("notifications.destination.http-webhook")
: $t("notifications.destination.dozzle-cloud")
}}
</p>
</div>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm btn-square">
<ion:ellipsis-vertical />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-100 border-base-content/20 z-50 w-40 border p-1 shadow-sm"
>
<li>
<a @click="editDestination">{{ $t("notifications.destination.edit") }}</a>
</li>
<li>
<a class="text-error" @click="deleteDestination">{{ $t("notifications.destination.delete") }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useMutation, useQuery } from "@urql/vue";
import { DeleteDispatcherDocument, GetDispatchersDocument, type Dispatcher } from "@/types/graphql";
import DestinationForm from "./DestinationForm.vue";
const { destination } = defineProps<{
destination: Dispatcher;
}>();
const showDrawer = useDrawer();
const deleteMutation = useMutation(DeleteDispatcherDocument);
const dispatchersQuery = useQuery({ query: GetDispatchersDocument, pause: true });
function editDestination() {
showDrawer(
DestinationForm,
{
destination,
onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }),
},
"md",
);
}
async function deleteDestination() {
await deleteMutation.executeMutation({ id: destination.id });
dispatchersQuery.executeQuery({ requestPolicy: "network-only" });
}
</script>
@@ -0,0 +1,179 @@
<template>
<div class="space-y-6 p-4">
<div class="mb-6">
<h2 class="text-2xl font-bold">
{{
isEditing
? $t("notifications.destination-form.edit-title")
: $t("notifications.destination-form.create-title")
}}
</h2>
<p class="text-base-content/60">{{ $t("notifications.destination-form.description") }}</p>
</div>
<!-- Name -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.name") }}</legend>
<input
ref="nameInput"
v-model="name"
type="text"
class="input focus:input-primary w-full text-base"
required
:class="{ 'input-primary': name.trim().length > 0 }"
:placeholder="$t('notifications.destination-form.name-placeholder')"
/>
</fieldset>
<!-- Type Selection -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.type") }}</legend>
<div class="space-y-3">
<label
class="card card-border 20 cursor-pointer transition-colors"
:class="type === 'webhook' ? 'border-primary bg-primary/10' : ''"
>
<div class="card-body flex-row items-center gap-3 p-4">
<input type="radio" v-model="type" value="webhook" class="radio radio-primary" />
<div>
<div class="font-semibold">{{ $t("notifications.destination-form.webhook-title") }}</div>
<div class="text-base-content/60 text-sm">
{{ $t("notifications.destination-form.webhook-description") }}
</div>
</div>
</div>
</label>
<label
class="card card-border border-base-content/20 cursor-pointer transition-colors"
:class="type === 'cloud' ? 'border-primary bg-primary/10' : ''"
>
<div class="card-body flex-row items-center gap-3 p-4">
<input type="radio" v-model="type" value="cloud" class="radio radio-primary" />
<div>
<div class="font-semibold">{{ $t("notifications.destination-form.cloud-title") }}</div>
<div class="text-base-content/60 text-sm">
{{ $t("notifications.destination-form.cloud-description") }}
</div>
</div>
</div>
</label>
</div>
</fieldset>
<!-- Webhook URL (only for webhook type) -->
<fieldset v-if="type === 'webhook'" class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.webhook-url") }}</legend>
<input
v-model="webhookUrl"
type="url"
class="input focus:input-primary w-full text-base"
:class="{ 'input-primary': isValidUrl, 'input-error': webhookUrl.trim() && !isValidUrl }"
:placeholder="$t('notifications.destination-form.webhook-url-placeholder')"
/>
</fieldset>
<!-- Error -->
<div v-if="error" class="alert alert-error">
<span>{{ error }}</span>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 pt-4">
<button class="btn" @click="testDestination" :disabled="!canTest || isTesting">
<span v-if="isTesting" class="loading loading-spinner loading-sm"></span>
{{ $t("notifications.destination-form.test") }}
</button>
<div class="flex-1"></div>
<button class="btn" @click="close?.()">{{ $t("notifications.destination-form.cancel") }}</button>
<button class="btn btn-primary" :disabled="!canSave" @click="saveDestination">
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
{{ isEditing ? $t("notifications.destination-form.save") : $t("notifications.destination-form.add") }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useMutation } from "@urql/vue";
import { CreateDispatcherDocument, UpdateDispatcherDocument, type Dispatcher } from "@/types/graphql";
const { close, onCreated, destination } = defineProps<{
close?: () => void;
onCreated?: () => void;
destination?: Dispatcher;
}>();
const createMutation = useMutation(CreateDispatcherDocument);
const updateMutation = useMutation(UpdateDispatcherDocument);
const isEditing = computed(() => !!destination);
const nameInput = ref<HTMLInputElement>();
const name = ref(destination?.name ?? "");
useFocus(nameInput, { initialValue: true });
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
const webhookUrl = ref(destination?.url ?? "");
const isTesting = ref(false);
const isSaving = ref(false);
const error = ref<string | null>(null);
const canTest = computed(() => {
if (type.value === "webhook") {
return webhookUrl.value.trim().length > 0;
}
return false;
});
const isValidUrl = computed(() => {
try {
new URL(webhookUrl.value.trim());
return true;
} catch {
return false;
}
});
const canSave = computed(() => {
if (isSaving.value) return false;
if (!name.value.trim()) return false;
if (type.value === "webhook" && !isValidUrl.value) return false;
return true;
});
async function testDestination() {
isTesting.value = true;
// TODO: Implement actual test when backend is ready
await new Promise((resolve) => setTimeout(resolve, 1000));
isTesting.value = false;
}
async function saveDestination() {
if (!canSave.value) return;
isSaving.value = true;
error.value = null;
try {
const input = {
name: name.value.trim(),
type: type.value,
url: type.value === "webhook" ? webhookUrl.value.trim() : undefined,
};
const result = isEditing.value
? await updateMutation.executeMutation({ id: destination!.id, input })
: await createMutation.executeMutation({ input });
if (result.error) {
throw new Error(result.error.message);
}
onCreated?.();
close?.();
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to save destination";
} finally {
isSaving.value = false;
}
}
</script>
+6 -1
View File
@@ -11,7 +11,7 @@
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open"></slot>
<slot v-if="open" :close="close"></slot>
</div>
</div>
<form method="dialog" class="modal-backdrop">
@@ -28,11 +28,16 @@ const { width } = defineProps<{
width: DrawerWidth;
}>();
function close() {
panel.value?.close();
}
defineExpose({
open: () => {
open.value = true;
panel.value?.showModal();
},
close,
});
useEventListener(panel, "close", () => (open.value = false));
+166
View File
@@ -0,0 +1,166 @@
import type { Completion } from "@codemirror/autocomplete";
export interface ExprEditorOptions {
parent: HTMLElement;
placeholder: string;
initialValue: string;
getHints: () => Completion[];
onChange?: (value: string) => void;
}
// Common operators for expr language
const exprOperators: Completion[] = [
{ label: "==", detail: "equals", type: "operator" },
{ label: "!=", detail: "not equals", type: "operator" },
{ label: "contains", detail: "string contains", type: "keyword" },
{ label: "startsWith", detail: "string starts with", type: "keyword" },
{ label: "endsWith", detail: "string ends with", type: "keyword" },
{ label: "matches", detail: "regex match", type: "keyword" },
{ label: "&&", detail: "logical AND", type: "operator" },
{ label: "||", detail: "logical OR", type: "operator" },
{ label: "!", detail: "logical NOT", type: "operator" },
{ label: "in", detail: "membership test", type: "keyword" },
{ label: "not in", detail: "negative membership", type: "keyword" },
];
export function createContainerHints(
containerNames: string[],
imageNames: string[],
hostNames: string[],
): Completion[] {
return [
{ label: "name", detail: "container name", type: "property" },
{ label: "id", detail: "container ID", type: "property" },
{ label: "image", detail: "container image", type: "property" },
{ label: "state", detail: "running, exited, etc.", type: "property" },
{ label: "health", detail: "healthy, unhealthy, none", type: "property" },
{ label: "host", detail: "docker host", type: "property" },
{ label: "labels", detail: "container labels map", type: "property" },
...exprOperators,
{ label: '"running"', detail: "state value", type: "string" },
{ label: '"exited"', detail: "state value", type: "string" },
{ label: '"created"', detail: "state value", type: "string" },
{ label: '"paused"', detail: "state value", type: "string" },
{ label: '"healthy"', detail: "health value", type: "string" },
{ label: '"unhealthy"', detail: "health value", type: "string" },
{ label: '"none"', detail: "health value", type: "string" },
...containerNames.map((name) => ({ label: `"${name}"`, detail: "container name", type: "string" }) as Completion),
...imageNames.map((image) => ({ label: `"${image}"`, detail: "image name", type: "string" }) as Completion),
...hostNames.map((host) => ({ label: `"${host}"`, detail: "host name", type: "string" }) as Completion),
];
}
export function createLogHints(): Completion[] {
return [
{ label: "message", detail: "log message content", type: "property" },
{ label: "level", detail: "log level", type: "property" },
{ label: "stream", detail: "stdout or stderr", type: "property" },
{ label: "type", detail: "log type", type: "property" },
{ label: "timestamp", detail: "unix timestamp", type: "property" },
{ label: "id", detail: "log entry ID", type: "property" },
...exprOperators,
{ label: '"error"', detail: "level value", type: "string" },
{ label: '"warn"', detail: "level value", type: "string" },
{ label: '"info"', detail: "level value", type: "string" },
{ label: '"debug"', detail: "level value", type: "string" },
{ label: '"trace"', detail: "level value", type: "string" },
{ label: '"stdout"', detail: "stream value", type: "string" },
{ label: '"stderr"', detail: "stream value", type: "string" },
{ label: 'level == "error"', detail: "match error logs", type: "text", boost: 10 },
{ label: 'message contains ""', detail: "search in message", type: "text", boost: 10 },
{ label: 'stream == "stderr"', detail: "match stderr", type: "text", boost: 10 },
];
}
function createAutocomplete(getHints: () => Completion[]) {
return (context: any) => {
const word = context.matchBefore(/[\w"=!&|]+/);
if (!word && !context.explicit) return null;
const currentWord = word ? word.text.toLowerCase() : "";
const hints = getHints();
const filtered = currentWord ? hints.filter((h) => h.label.toLowerCase().includes(currentWord)) : hints;
return { from: word ? word.from : context.pos, options: filtered };
};
}
export async function createExprEditor(options: ExprEditorOptions) {
const [
{ EditorView, keymap, placeholder },
{ EditorState },
{ autocompletion, completionKeymap },
{ HighlightStyle, syntaxHighlighting },
{ tags },
] = await Promise.all([
import("@codemirror/view"),
import("@codemirror/state"),
import("@codemirror/autocomplete"),
import("@codemirror/language"),
import("@lezer/highlight"),
]);
const editorTheme = EditorView.theme({
"&": {
backgroundColor: "var(--color-base-100)",
color: "var(--color-base-content)",
},
".cm-content": {
caretColor: "var(--color-primary)",
},
".cm-cursor": {
borderLeftColor: "var(--color-primary)",
},
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
backgroundColor: "var(--color-base-300)",
},
".cm-activeLine": {
backgroundColor: "color-mix(in oklch, var(--color-base-200) 50%, transparent)",
},
".cm-gutters": {
backgroundColor: "var(--color-base-200)",
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
border: "none",
},
".cm-activeLineGutter": {
backgroundColor: "var(--color-base-300)",
},
});
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: "var(--color-primary)" },
{ tag: tags.operator, color: "var(--color-secondary)" },
{ tag: tags.string, color: "var(--color-success)" },
{ tag: tags.number, color: "var(--color-warning)" },
{ tag: tags.bool, color: "var(--color-warning)" },
{ tag: tags.propertyName, color: "var(--color-info)" },
{ tag: tags.variableName, color: "var(--color-base-content)" },
{
tag: tags.comment,
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
fontStyle: "italic",
},
]);
const state = EditorState.create({
doc: options.initialValue,
extensions: [
EditorView.lineWrapping,
placeholder(options.placeholder),
autocompletion({
override: [createAutocomplete(options.getHints)],
activateOnTyping: true,
}),
keymap.of(completionKeymap),
editorTheme,
syntaxHighlighting(highlightStyle),
EditorView.updateListener.of((update) => {
if (update.docChanged && options.onChange) {
options.onChange(update.view.state.doc.toString());
}
}),
],
});
return new EditorView({ state, parent: options.parent });
}
+120
View File
@@ -0,0 +1,120 @@
query GetNotificationRules {
notificationRules {
id
name
enabled
containerExpression
logExpression
triggerCount
triggeredContainers
lastTriggeredAt
dispatcher {
id
name
type
url
}
}
}
query GetDispatchers {
dispatchers {
id
name
type
url
}
}
mutation CreateNotificationRule($input: NotificationRuleInput!) {
createNotificationRule(input: $input) {
id
name
enabled
containerExpression
logExpression
dispatcher {
id
name
type
}
}
}
mutation UpdateNotificationRule($id: Int!, $input: NotificationRuleUpdateInput!) {
updateNotificationRule(id: $id, input: $input) {
id
name
enabled
containerExpression
logExpression
dispatcher {
id
name
type
}
}
}
mutation ReplaceNotificationRule($id: Int!, $input: NotificationRuleInput!) {
replaceNotificationRule(id: $id, input: $input) {
id
name
enabled
containerExpression
logExpression
dispatcher {
id
name
type
}
}
}
mutation DeleteNotificationRule($id: Int!) {
deleteNotificationRule(id: $id)
}
mutation CreateDispatcher($input: DispatcherInput!) {
createDispatcher(input: $input) {
id
name
type
url
}
}
mutation UpdateDispatcher($id: Int!, $input: DispatcherInput!) {
updateDispatcher(id: $id, input: $input) {
id
name
type
url
}
}
mutation DeleteDispatcher($id: Int!) {
deleteDispatcher(id: $id)
}
mutation PreviewExpression($input: PreviewInput!) {
previewExpression(input: $input) {
containerError
logError
matchedContainers {
id
name
image
host
}
matchedLogs {
id
type
message
timestamp
level
stream
}
totalLogs
}
}
+3 -3
View File
@@ -42,10 +42,10 @@
<button>close</button>
</form>
</dialog>
<SideDrawer ref="drawer" :width="drawerWidth">
<SideDrawer ref="drawer" :width="drawerWidth" v-slot="{ close }">
<Suspense :timeout="0">
<component :is="drawerComponent" v-bind="drawerProperties" />
<template #fallback> Loading dependencies... </template>
<component :is="drawerComponent" v-bind="drawerProperties" :close="close" />
<template #fallback> <span class="loading loading-spinner loading-sm"></span></template>
</Suspense>
</SideDrawer>
<ToastModal />
+41
View File
@@ -151,3 +151,44 @@ body {
.splitpanes--vertical .splitpanes__pane {
transition: none !important;
}
/* CodeMirror autocomplete tooltip styles */
.cm-tooltip {
@apply bg-base-200! border-base-content/40! min-w-96 rounded-sm border shadow-md;
}
.cm-tooltip-autocomplete ul {
@apply font-sans;
}
.cm-tooltip-autocomplete ul li {
@apply my-1 px-2;
}
.cm-tooltip-autocomplete ul li[aria-selected] {
@apply bg-primary/20 text-base-content!;
}
.cm-completionLabel {
@apply text-base-content!;
}
.cm-completionDetail {
@apply text-base-content/60! ml-2 italic;
}
.cm-completionMatchedText {
@apply text-primary! font-bold no-underline;
}
.cm-completionIcon {
@apply mr-2 opacity-70;
}
.cm-editor.cm-focused {
outline: none;
}
.cm-scroller {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
+21 -1
View File
@@ -1,4 +1,4 @@
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
import type { ContainerHealth, ContainerJson, ContainerStat, ContainerState } from "@/types/Container";
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
import { Ref } from "vue";
@@ -122,4 +122,24 @@ export class Container {
this._stat = stat;
}
}
static fromJSON(c: ContainerJson): Container {
return new Container(
c.id,
new Date(c.created),
new Date(c.startedAt),
new Date(c.finishedAt),
c.image,
c.name,
c.command,
c.host,
c.labels,
c.state,
c.cpuLimit,
c.memoryLimit,
c.stats ?? [],
c.group,
c.health,
);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { type App } from "vue";
import urql, { cacheExchange, fetchExchange } from "@urql/vue";
import { withBase } from "@/stores/config";
export const install = (app: App) => {
app.use(urql, {
url: withBase("/api/graphql"),
exchanges: [cacheExchange, fetchExchange],
});
};
+105
View File
@@ -0,0 +1,105 @@
<template>
<PageWithLinks>
<section>
<!-- Header -->
<div class="mb-8">
<h2 class="text-2xl font-bold">{{ $t("notifications.title") }}</h2>
<p class="text-base-content/60">{{ $t("notifications.description") }}</p>
</div>
<!-- Destinations Section -->
<div class="mb-8">
<h3 class="text-base-content/60 mb-4 font-semibold tracking-wide uppercase">
{{ $t("notifications.destinations") }}
</h3>
<div class="flex flex-wrap gap-4">
<DestinationCard v-for="dest in dispatchers" :key="dest.id" :destination="dest" class="w-full md:w-72" />
<!-- Add Destination Card -->
<button
class="card card-border border-base-content/30 hover:border-base-content/50 w-full cursor-pointer border-dashed transition-colors md:w-72"
@click="openAddDestination"
>
<div class="card-body items-center justify-center gap-1 p-4">
<mdi:plus class="text-2xl" />
<span class="text-base-content/60 text-sm">{{ $t("notifications.add-destination") }}</span>
</div>
</button>
</div>
</div>
<!-- Alerts Section -->
<div>
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base-content/60 font-semibold tracking-wide uppercase">{{ $t("notifications.alerts") }}</h3>
<button class="btn btn-ghost text-primary" @click="openCreateAlert">
<mdi:plus />
{{ $t("notifications.add") }}
</button>
</div>
<!-- Filter Tabs -->
<div class="tabs tabs-box mb-6">
<button class="tab" :class="{ 'tab-active': filter === 'all' }" @click="filter = 'all'">
{{ $t("notifications.filter.all", { count: alerts.length }) }}
</button>
<button class="tab" :class="{ 'tab-active': filter === 'enabled' }" @click="filter = 'enabled'">
{{ $t("notifications.filter.enabled", { count: enabledCount }) }}
</button>
<button class="tab" :class="{ 'tab-active': filter === 'paused' }" @click="filter = 'paused'">
{{ $t("notifications.filter.paused", { count: pausedCount }) }}
</button>
</div>
<!-- Alerts List -->
<div v-if="!alerts.length" class="text-base-content/60 py-4">
{{ $t("notifications.no-alerts") }}
</div>
<div v-else class="space-y-4">
<AlertCard v-for="alert in filteredAlerts" :key="alert.id" :alert="alert" />
</div>
</div>
</section>
</PageWithLinks>
</template>
<script lang="ts" setup>
import { useQuery } from "@urql/vue";
import { GetNotificationRulesDocument, GetDispatchersDocument, type NotificationRule } from "@/types/graphql";
import AlertForm from "@/components/Notification/AlertForm.vue";
import DestinationForm from "@/components/Notification/DestinationForm.vue";
import DestinationCard from "@/components/Notification/DestinationCard.vue";
const showDrawer = useDrawer();
// GraphQL queries
const alertsQuery = useQuery({ query: GetNotificationRulesDocument });
const dispatchersQuery = useQuery({ query: GetDispatchersDocument });
// Computed data from queries
const alerts = computed(() => alertsQuery.data.value?.notificationRules ?? []);
const dispatchers = computed(() => dispatchersQuery.data.value?.dispatchers ?? []);
// Local state
const filter = ref<"all" | "enabled" | "paused">("all");
const enabledCount = computed(() => alerts.value.filter((a: NotificationRule) => a.enabled).length);
const pausedCount = computed(() => alerts.value.filter((a: NotificationRule) => !a.enabled).length);
const filteredAlerts = computed(() => {
if (filter.value === "enabled") return alerts.value.filter((a: NotificationRule) => a.enabled);
if (filter.value === "paused") return alerts.value.filter((a: NotificationRule) => !a.enabled);
return alerts.value;
});
function openCreateAlert() {
showDrawer(AlertForm, { onCreated: () => alertsQuery.executeQuery({ requestPolicy: "network-only" }) }, "lg");
}
function openAddDestination() {
showDrawer(
DestinationForm,
{ onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }) },
"md",
);
}
</script>
+1 -22
View File
@@ -140,28 +140,7 @@ export const useContainerStore = defineStore("container", () => {
existing.name = c.name;
});
containers.value = [
...containers.value,
...newContainers.map((c) => {
return new Container(
c.id,
new Date(c.created),
new Date(c.startedAt),
new Date(c.finishedAt),
c.image,
c.name,
c.command,
c.host,
c.labels,
c.state,
c.cpuLimit,
c.memoryLimit,
c.stats,
c.group,
c.health,
);
}),
];
containers.value = [...containers.value, ...newContainers.map(Container.fromJSON)];
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
+13
View File
@@ -86,6 +86,13 @@ declare module 'vue-router/auto-routes' {
{ name: ParamValue<false> },
| never
>,
'/notifications': RouteRecordInfo<
'/notifications',
'/notifications',
Record<never, never>,
Record<never, never>,
| never
>,
'/owner/[name]': RouteRecordInfo<
'/owner/[name]',
'/owner/:name',
@@ -188,6 +195,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'assets/pages/notifications.vue': {
routes:
| '/notifications'
views:
| never
}
'assets/pages/owner/[name].vue': {
routes:
| '/owner/[name]'
+802
View File
@@ -0,0 +1,802 @@
import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string };
String: { input: string; output: string };
Boolean: { input: boolean; output: boolean };
Int: { input: number; output: number };
Float: { input: number; output: number };
Any: { input: unknown; output: unknown };
Int64: { input: number; output: number };
Map: { input: Record<string, string>; output: Record<string, string> };
Time: { input: string; output: string };
};
export type Container = {
__typename?: "Container";
command: Scalars["String"]["output"];
created: Scalars["Time"]["output"];
group?: Maybe<Scalars["String"]["output"]>;
health?: Maybe<Scalars["String"]["output"]>;
host?: Maybe<Scalars["String"]["output"]>;
id: Scalars["String"]["output"];
image: Scalars["String"]["output"];
labels?: Maybe<Scalars["Map"]["output"]>;
name: Scalars["String"]["output"];
startedAt: Scalars["Time"]["output"];
state: Scalars["String"]["output"];
};
export type Dispatcher = {
__typename?: "Dispatcher";
id: Scalars["Int"]["output"];
name: Scalars["String"]["output"];
type: Scalars["String"]["output"];
url?: Maybe<Scalars["String"]["output"]>;
};
export type DispatcherInput = {
name: Scalars["String"]["input"];
type: Scalars["String"]["input"];
url?: InputMaybe<Scalars["String"]["input"]>;
};
export type LogEvent = {
__typename?: "LogEvent";
containerId?: Maybe<Scalars["String"]["output"]>;
id: Scalars["Int"]["output"];
level?: Maybe<Scalars["String"]["output"]>;
message?: Maybe<Scalars["Any"]["output"]>;
stream?: Maybe<Scalars["String"]["output"]>;
timestamp: Scalars["Int64"]["output"];
type?: Maybe<Scalars["String"]["output"]>;
};
export type Mutation = {
__typename?: "Mutation";
createDispatcher: Dispatcher;
createNotificationRule: NotificationRule;
deleteDispatcher: Scalars["Boolean"]["output"];
deleteNotificationRule: Scalars["Boolean"]["output"];
previewExpression: PreviewResult;
replaceNotificationRule: NotificationRule;
updateDispatcher: Dispatcher;
updateNotificationRule: NotificationRule;
};
export type MutationCreateDispatcherArgs = {
input: DispatcherInput;
};
export type MutationCreateNotificationRuleArgs = {
input: NotificationRuleInput;
};
export type MutationDeleteDispatcherArgs = {
id: Scalars["Int"]["input"];
};
export type MutationDeleteNotificationRuleArgs = {
id: Scalars["Int"]["input"];
};
export type MutationPreviewExpressionArgs = {
input: PreviewInput;
};
export type MutationReplaceNotificationRuleArgs = {
id: Scalars["Int"]["input"];
input: NotificationRuleInput;
};
export type MutationUpdateDispatcherArgs = {
id: Scalars["Int"]["input"];
input: DispatcherInput;
};
export type MutationUpdateNotificationRuleArgs = {
id: Scalars["Int"]["input"];
input: NotificationRuleUpdateInput;
};
export type NotificationRule = {
__typename?: "NotificationRule";
containerExpression: Scalars["String"]["output"];
dispatcher?: Maybe<Dispatcher>;
enabled: Scalars["Boolean"]["output"];
id: Scalars["Int"]["output"];
lastTriggeredAt?: Maybe<Scalars["Time"]["output"]>;
logExpression: Scalars["String"]["output"];
name: Scalars["String"]["output"];
triggerCount: Scalars["Int64"]["output"];
triggeredContainers: Scalars["Int"]["output"];
};
export type NotificationRuleInput = {
containerExpression: Scalars["String"]["input"];
dispatcherId: Scalars["Int"]["input"];
enabled: Scalars["Boolean"]["input"];
logExpression: Scalars["String"]["input"];
name: Scalars["String"]["input"];
};
export type NotificationRuleUpdateInput = {
containerExpression?: InputMaybe<Scalars["String"]["input"]>;
dispatcherId?: InputMaybe<Scalars["Int"]["input"]>;
enabled?: InputMaybe<Scalars["Boolean"]["input"]>;
logExpression?: InputMaybe<Scalars["String"]["input"]>;
name?: InputMaybe<Scalars["String"]["input"]>;
};
export type PreviewInput = {
containerExpression: Scalars["String"]["input"];
logExpression?: InputMaybe<Scalars["String"]["input"]>;
};
export type PreviewResult = {
__typename?: "PreviewResult";
containerError?: Maybe<Scalars["String"]["output"]>;
logError?: Maybe<Scalars["String"]["output"]>;
matchedContainers: Array<Container>;
matchedLogs: Array<LogEvent>;
totalLogs: Scalars["Int"]["output"];
};
export type Query = {
__typename?: "Query";
dispatcher?: Maybe<Dispatcher>;
dispatchers: Array<Dispatcher>;
notificationRule?: Maybe<NotificationRule>;
notificationRules: Array<NotificationRule>;
};
export type QueryDispatcherArgs = {
id: Scalars["Int"]["input"];
};
export type QueryNotificationRuleArgs = {
id: Scalars["Int"]["input"];
};
export type GetNotificationRulesQueryVariables = Exact<{ [key: string]: never }>;
export type GetNotificationRulesQuery = {
__typename?: "Query";
notificationRules: Array<{
__typename?: "NotificationRule";
id: number;
name: string;
enabled: boolean;
containerExpression: string;
logExpression: string;
triggerCount: number;
triggeredContainers: number;
lastTriggeredAt?: string | null;
dispatcher?: { __typename?: "Dispatcher"; id: number; name: string; type: string; url?: string | null } | null;
}>;
};
export type GetDispatchersQueryVariables = Exact<{ [key: string]: never }>;
export type GetDispatchersQuery = {
__typename?: "Query";
dispatchers: Array<{ __typename?: "Dispatcher"; id: number; name: string; type: string; url?: string | null }>;
};
export type CreateNotificationRuleMutationVariables = Exact<{
input: NotificationRuleInput;
}>;
export type CreateNotificationRuleMutation = {
__typename?: "Mutation";
createNotificationRule: {
__typename?: "NotificationRule";
id: number;
name: string;
enabled: boolean;
containerExpression: string;
logExpression: string;
dispatcher?: { __typename?: "Dispatcher"; id: number; name: string; type: string } | null;
};
};
export type UpdateNotificationRuleMutationVariables = Exact<{
id: Scalars["Int"]["input"];
input: NotificationRuleUpdateInput;
}>;
export type UpdateNotificationRuleMutation = {
__typename?: "Mutation";
updateNotificationRule: {
__typename?: "NotificationRule";
id: number;
name: string;
enabled: boolean;
containerExpression: string;
logExpression: string;
dispatcher?: { __typename?: "Dispatcher"; id: number; name: string; type: string } | null;
};
};
export type ReplaceNotificationRuleMutationVariables = Exact<{
id: Scalars["Int"]["input"];
input: NotificationRuleInput;
}>;
export type ReplaceNotificationRuleMutation = {
__typename?: "Mutation";
replaceNotificationRule: {
__typename?: "NotificationRule";
id: number;
name: string;
enabled: boolean;
containerExpression: string;
logExpression: string;
dispatcher?: { __typename?: "Dispatcher"; id: number; name: string; type: string } | null;
};
};
export type DeleteNotificationRuleMutationVariables = Exact<{
id: Scalars["Int"]["input"];
}>;
export type DeleteNotificationRuleMutation = { __typename?: "Mutation"; deleteNotificationRule: boolean };
export type CreateDispatcherMutationVariables = Exact<{
input: DispatcherInput;
}>;
export type CreateDispatcherMutation = {
__typename?: "Mutation";
createDispatcher: { __typename?: "Dispatcher"; id: number; name: string; type: string; url?: string | null };
};
export type UpdateDispatcherMutationVariables = Exact<{
id: Scalars["Int"]["input"];
input: DispatcherInput;
}>;
export type UpdateDispatcherMutation = {
__typename?: "Mutation";
updateDispatcher: { __typename?: "Dispatcher"; id: number; name: string; type: string; url?: string | null };
};
export type DeleteDispatcherMutationVariables = Exact<{
id: Scalars["Int"]["input"];
}>;
export type DeleteDispatcherMutation = { __typename?: "Mutation"; deleteDispatcher: boolean };
export type PreviewExpressionMutationVariables = Exact<{
input: PreviewInput;
}>;
export type PreviewExpressionMutation = {
__typename?: "Mutation";
previewExpression: {
__typename?: "PreviewResult";
containerError?: string | null;
logError?: string | null;
totalLogs: number;
matchedContainers: Array<{
__typename?: "Container";
id: string;
name: string;
image: string;
host?: string | null;
}>;
matchedLogs: Array<{
__typename?: "LogEvent";
id: number;
type?: string | null;
message?: unknown | null;
timestamp: number;
level?: string | null;
stream?: string | null;
}>;
};
};
export const GetNotificationRulesDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "query",
name: { kind: "Name", value: "GetNotificationRules" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "notificationRules" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "enabled" } },
{ kind: "Field", name: { kind: "Name", value: "containerExpression" } },
{ kind: "Field", name: { kind: "Name", value: "logExpression" } },
{ kind: "Field", name: { kind: "Name", value: "triggerCount" } },
{ kind: "Field", name: { kind: "Name", value: "triggeredContainers" } },
{ kind: "Field", name: { kind: "Name", value: "lastTriggeredAt" } },
{
kind: "Field",
name: { kind: "Name", value: "dispatcher" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
{ kind: "Field", name: { kind: "Name", value: "url" } },
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<GetNotificationRulesQuery, GetNotificationRulesQueryVariables>;
export const GetDispatchersDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "query",
name: { kind: "Name", value: "GetDispatchers" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "dispatchers" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
{ kind: "Field", name: { kind: "Name", value: "url" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<GetDispatchersQuery, GetDispatchersQueryVariables>;
export const CreateNotificationRuleDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "CreateNotificationRule" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "NotificationRuleInput" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "createNotificationRule" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "enabled" } },
{ kind: "Field", name: { kind: "Name", value: "containerExpression" } },
{ kind: "Field", name: { kind: "Name", value: "logExpression" } },
{
kind: "Field",
name: { kind: "Name", value: "dispatcher" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<CreateNotificationRuleMutation, CreateNotificationRuleMutationVariables>;
export const UpdateNotificationRuleDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "UpdateNotificationRule" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } },
},
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "NotificationRuleUpdateInput" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "updateNotificationRule" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: { kind: "Variable", name: { kind: "Name", value: "id" } },
},
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "enabled" } },
{ kind: "Field", name: { kind: "Name", value: "containerExpression" } },
{ kind: "Field", name: { kind: "Name", value: "logExpression" } },
{
kind: "Field",
name: { kind: "Name", value: "dispatcher" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<UpdateNotificationRuleMutation, UpdateNotificationRuleMutationVariables>;
export const ReplaceNotificationRuleDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "ReplaceNotificationRule" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } },
},
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "NotificationRuleInput" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "replaceNotificationRule" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: { kind: "Variable", name: { kind: "Name", value: "id" } },
},
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "enabled" } },
{ kind: "Field", name: { kind: "Name", value: "containerExpression" } },
{ kind: "Field", name: { kind: "Name", value: "logExpression" } },
{
kind: "Field",
name: { kind: "Name", value: "dispatcher" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<ReplaceNotificationRuleMutation, ReplaceNotificationRuleMutationVariables>;
export const DeleteNotificationRuleDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "DeleteNotificationRule" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "deleteNotificationRule" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: { kind: "Variable", name: { kind: "Name", value: "id" } },
},
],
},
],
},
},
],
} as unknown as DocumentNode<DeleteNotificationRuleMutation, DeleteNotificationRuleMutationVariables>;
export const CreateDispatcherDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "CreateDispatcher" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "DispatcherInput" } } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "createDispatcher" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
{ kind: "Field", name: { kind: "Name", value: "url" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<CreateDispatcherMutation, CreateDispatcherMutationVariables>;
export const UpdateDispatcherDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "UpdateDispatcher" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } },
},
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "DispatcherInput" } } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "updateDispatcher" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: { kind: "Variable", name: { kind: "Name", value: "id" } },
},
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
{ kind: "Field", name: { kind: "Name", value: "url" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<UpdateDispatcherMutation, UpdateDispatcherMutationVariables>;
export const DeleteDispatcherDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "DeleteDispatcher" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Int" } } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "deleteDispatcher" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: { kind: "Variable", name: { kind: "Name", value: "id" } },
},
],
},
],
},
},
],
} as unknown as DocumentNode<DeleteDispatcherMutation, DeleteDispatcherMutationVariables>;
export const PreviewExpressionDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "PreviewExpression" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "input" } },
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "PreviewInput" } } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "previewExpression" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "containerError" } },
{ kind: "Field", name: { kind: "Name", value: "logError" } },
{
kind: "Field",
name: { kind: "Name", value: "matchedContainers" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "image" } },
{ kind: "Field", name: { kind: "Name", value: "host" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "matchedLogs" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "type" } },
{ kind: "Field", name: { kind: "Name", value: "message" } },
{ kind: "Field", name: { kind: "Name", value: "timestamp" } },
{ kind: "Field", name: { kind: "Name", value: "level" } },
{ kind: "Field", name: { kind: "Name", value: "stream" } },
],
},
},
{ kind: "Field", name: { kind: "Name", value: "totalLogs" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<PreviewExpressionMutation, PreviewExpressionMutationVariables>;
+21
View File
@@ -0,0 +1,21 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "graph/schema.graphqls",
documents: ["assets/**/*.graphql"],
generates: {
"assets/types/graphql.ts": {
plugins: ["typescript", "typescript-operations", "typed-document-node"],
config: {
scalars: {
Time: "string",
Int64: "number",
Any: "unknown",
Map: "Record<string, string>",
},
},
},
},
};
export default config;
+14 -6
View File
@@ -8,7 +8,6 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
github.com/gogo/protobuf v1.3.2 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -23,8 +22,10 @@ require (
)
require (
github.com/99designs/gqlgen v0.17.86
github.com/PuerkitoBio/goquery v1.11.0
github.com/andybalholm/brotli v1.2.0
github.com/expr-lang/expr v1.17.7
github.com/go-chi/chi/v5 v5.2.4
github.com/go-chi/jwtauth/v5 v5.3.3
github.com/go-faker/faker/v4 v4.7.0
@@ -32,8 +33,10 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.3.0
github.com/rs/zerolog v1.34.0
github.com/samber/lo v1.52.0
github.com/vektah/gqlparser/v2 v2.5.31
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.16
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.47.0
golang.org/x/sync v0.19.0
google.golang.org/grpc v1.78.0
@@ -45,6 +48,7 @@ require (
)
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
@@ -64,12 +68,12 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
@@ -91,7 +95,9 @@ 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/sosodev/duration v1.3.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/urfave/cli/v3 v3.6.1 // 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
@@ -100,11 +106,12 @@ require (
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.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.40.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
@@ -114,9 +121,10 @@ require (
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
sigs.k8s.io/yaml v1.6.0 // indirect
)
go 1.25.6
tool github.com/99designs/gqlgen
+46 -397
View File
@@ -1,25 +1,27 @@
github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w=
github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
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/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
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=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -44,28 +46,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.1+incompatible h1:aTSWVTDStpHbnRu0xBcGoJEjRf5EQKt6nik6Vif8sWw=
github.com/docker/docker v28.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -74,37 +58,21 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
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=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo=
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs=
github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0=
github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k=
github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY=
github.com/go-faker/faker/v4 v4.6.2 h1:IR1uQUYotFZnuTL7Iuy0FDGtHM5Rt1Q+2nipH9gnqKs=
github.com/go-faker/faker/v4 v4.6.2/go.mod h1:u1dIRP5neLB6kTzgyVjdBOV5R1uP7BdxkcWk7tiKQXk=
github.com/go-faker/faker/v4 v4.7.0 h1:VboC02cXHl/NuQh5lM2W8b87yp4iFXIu59x4w0RZi4E=
github.com/go-faker/faker/v4 v4.7.0/go.mod h1:u1dIRP5neLB6kTzgyVjdBOV5R1uP7BdxkcWk7tiKQXk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -117,43 +85,37 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -195,7 +157,6 @@ github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXy
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
@@ -205,10 +166,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -219,25 +180,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v4 v4.0.0 h1:F1za+MBXzDQtQq+OVgFsojSX4w66rsNDmQNebPFAncA=
github.com/puzpuzpuz/xsync/v4 v4.0.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -246,13 +195,11 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
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=
@@ -263,127 +210,62 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
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=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
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=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -392,59 +274,21 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -457,22 +301,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@@ -484,22 +312,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -511,24 +323,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
@@ -536,74 +330,28 @@ golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
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=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
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=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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=
@@ -616,124 +364,25 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk=
k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc=
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
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=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s=
k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
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=
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw=
k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY=
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
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=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
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=
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM=
k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg=
k8s.io/metrics v0.33.2 h1:gNCBmtnUMDMCRg9Ly5ehxP3OdKISMsOnh1vzk01iCgE=
k8s.io/metrics v0.33.2/go.mod h1:yxoAosKGRsZisv3BGekC5W6T1J8XSV+PoUEevACRv7c=
k8s.io/metrics v0.33.3 h1:9CcqBz15JZfISqwca33gdHS8I6XfsK1vA8WUdEnG70g=
k8s.io/metrics v0.33.3/go.mod h1:Aw+cdg4AYHw0HvUY+lCyq40FOO84awrqvJRTw0cmXDs=
k8s.io/metrics v0.33.4 h1:eJ6UdTpKTUQVZbKpUdm5ve39aPpAvvNwLrs13oQcWKc=
k8s.io/metrics v0.33.4/go.mod h1:NO/lgFtyIPTurz56debdSh5qRqRfpO8MlkMpau1Ue8U=
k8s.io/metrics v0.34.0 h1:nYSfG2+tnL6/MRC2I+sGHjtNEGoEoM/KktgGOoQFwws=
k8s.io/metrics v0.34.0/go.mod h1:KCuXmotE0v4AvoARKUP8NC4lUnbK/Du1mluGdor5h4M=
k8s.io/metrics v0.34.1 h1:374Rexmp1xxgRt64Bi0TsjAM8cA/Y8skwCoPdjtIslE=
k8s.io/metrics v0.34.1/go.mod h1:Drf5kPfk2NJrlpcNdSiAAHn/7Y9KqxpRNagByM7Ei80=
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=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+174
View File
@@ -0,0 +1,174 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
# Where should the generated server code go?
exec:
package: graph
layout: single-file # Only other option is "follow-schema," ie multi-file.
# Only for single-file layout:
filename: graph/generated.go
# Only for follow-schema layout:
# dir: graph
# filename_template: "{name}.generated.go"
# Optional: Maximum number of goroutines in concurrency to use per child resolvers(default: unlimited)
# worker_limit: 1000
# Uncomment to enable federation
# federation:
# filename: graph/federation.go
# package: graph
# version: 2
# options:
# computed_requires: true
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model
# Optional: Pass in a path to a new gotpl template to use for generating the models
# model_template: [your/path/model.gotpl]
# Where should the resolver implementations go?
resolver:
package: graph
layout: follow-schema # Only other option is "single-file."
# Only for single-file layout:
# filename: graph/resolver.go
# Only for follow-schema layout:
dir: graph
filename_template: "{name}.resolvers.go"
# Optional: turn on to not generate template comments above resolvers
# omit_template_comment: false
# Optional: Pass in a path to a new gotpl template to use for generating resolvers
# resolver_template: [your/path/resolver.gotpl]
# Optional: turn on to avoid rewriting existing resolver(s) when generating
# preserve_resolver: false
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: turn on to omit Is<Name>() methods to interface and unions
# omit_interface_checks: true
# Optional: turn on to skip generation of ComplexityRoot struct content and Complexity function
# omit_complexity: false
# Optional: turn on to not generate any file notice comments in generated files
# omit_gqlgen_file_notice: false
# Optional: turn on to exclude the gqlgen version in the generated file notice. No effect if `omit_gqlgen_file_notice` is true.
# omit_gqlgen_version_in_file_notice: false
# Optional: turn on to exclude root models such as Query and Mutation from the generated models file.
# omit_root_models: false
# Optional: turn on to exclude resolver fields from the generated models file.
# omit_resolver_fields: false
# Optional: turn off to make struct-type struct fields not use pointers
# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing }
# struct_fields_always_pointers: true
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true
# Optional: turn on to return pointers instead of values in unmarshalInput
# return_pointers_in_unmarshalinput: false
# Optional: wrap nullable input fields with Omittable
# nullable_input_omittable: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# Optional: set to skip running `go mod tidy` when generating server code
# skip_mod_tidy: true
# Optional: if this is set to true, argument directives that
# decorate a field with a null value will still be called.
#
# This enables argumment directives to not just mutate
# argument values but to set them even if they're null.
call_argument_directives_with_null: true
# This enables gql server to use function syntax for execution context
# instead of generating receiver methods of the execution context.
# use_function_syntax_for_execution_context: true
# Optional: set build tags that will be used to load packages
# go_build_tags:
# - private
# - enterprise
# Optional: set to modify the initialisms regarded for Go names
# go_initialisms:
# replace_defaults: false # if true, the default initialisms will get dropped in favor of the new ones instead of being added
# initialisms: # List of initialisms to for Go names
# - 'CC'
# - 'BCC'
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
# - "github.com/amir20/dozzle/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
# gqlgen provides a default GraphQL UUID convenience wrapper for github.com/google/uuid
# but you can override this to provide your own GraphQL UUID implementation
UUID:
model:
- github.com/99designs/gqlgen/graphql.UUID
# The GraphQL spec explicitly states that the Int type is a signed 32-bit
# integer. Using Go int or int64 to represent it can lead to unexpected
# behavior, and some GraphQL tools like Apollo Router will fail when
# communicating numbers that overflow 32-bits.
#
# You may choose to use the custom, built-in Int64 scalar to represent 64-bit
# integers, or ignore the spec and bind Int to graphql.Int / graphql.Int64
# (the default behavior of gqlgen). This is fine in simple use cases when you
# do not need to worry about interoperability and only expect small numbers.
Int:
model:
- github.com/99designs/gqlgen/graphql.Int32
Int64:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
Time:
model:
- github.com/99designs/gqlgen/graphql.Time
Any:
model:
- github.com/99designs/gqlgen/graphql.Any
Map:
model:
- github.com/99designs/gqlgen/graphql.Map
Container:
model:
- github.com/amir20/dozzle/internal/container.Container
LogEvent:
model:
- github.com/amir20/dozzle/internal/container.LogEvent
+6210
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
package graph
import (
"time"
"github.com/amir20/dozzle/graph/model"
"github.com/amir20/dozzle/internal/notification"
)
func subscriptionToNotificationRule(sub *notification.Subscription, dispatchers []notification.DispatcherConfig) *model.NotificationRule {
var lastTriggeredAt *time.Time
if t := sub.LastTriggeredAt.Load(); t != nil && !t.IsZero() {
lastTriggeredAt = t
}
// Find the dispatcher
var dispatcher *model.Dispatcher
for _, d := range dispatchers {
if d.ID == sub.DispatcherID {
dispatcher = dispatcherConfigToDispatcher(&d)
break
}
}
return &model.NotificationRule{
ID: int32(sub.ID),
Name: sub.Name,
Enabled: sub.Enabled,
Dispatcher: dispatcher,
LogExpression: sub.LogExpression,
ContainerExpression: sub.ContainerExpression,
TriggerCount: int(sub.TriggerCount.Load()),
LastTriggeredAt: lastTriggeredAt,
TriggeredContainers: int32(sub.TriggeredContainersCount()),
}
}
func dispatcherConfigToDispatcher(d *notification.DispatcherConfig) *model.Dispatcher {
var url *string
if d.URL != "" {
url = &d.URL
}
return &model.Dispatcher{
ID: int32(d.ID),
Name: d.Name,
Type: d.Type,
URL: url,
}
}
// Error is a simple error type for GraphQL errors
type Error struct {
Message string
}
func (e *Error) Error() string {
return e.Message
}
+69
View File
@@ -0,0 +1,69 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
import (
"time"
"github.com/amir20/dozzle/internal/container"
)
type Dispatcher struct {
ID int32 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
URL *string `json:"url,omitempty"`
}
type DispatcherInput struct {
Name string `json:"name"`
Type string `json:"type"`
URL *string `json:"url,omitempty"`
}
type Mutation struct {
}
type NotificationRule struct {
ID int32 `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Dispatcher *Dispatcher `json:"dispatcher,omitempty"`
LogExpression string `json:"logExpression"`
ContainerExpression string `json:"containerExpression"`
TriggerCount int `json:"triggerCount"`
LastTriggeredAt *time.Time `json:"lastTriggeredAt,omitempty"`
TriggeredContainers int32 `json:"triggeredContainers"`
}
type NotificationRuleInput struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
DispatcherID int32 `json:"dispatcherId"`
LogExpression string `json:"logExpression"`
ContainerExpression string `json:"containerExpression"`
}
type NotificationRuleUpdateInput struct {
Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
DispatcherID *int32 `json:"dispatcherId,omitempty"`
LogExpression *string `json:"logExpression,omitempty"`
ContainerExpression *string `json:"containerExpression,omitempty"`
}
type PreviewInput struct {
ContainerExpression string `json:"containerExpression"`
LogExpression *string `json:"logExpression,omitempty"`
}
type PreviewResult struct {
ContainerError *string `json:"containerError,omitempty"`
LogError *string `json:"logError,omitempty"`
MatchedContainers []*container.Container `json:"matchedContainers"`
MatchedLogs []*container.LogEvent `json:"matchedLogs"`
TotalLogs int32 `json:"totalLogs"`
}
type Query struct {
}
+37
View File
@@ -0,0 +1,37 @@
//go:generate go run github.com/99designs/gqlgen generate
package graph
import (
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
)
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type HostService interface {
// Notification rules (subscriptions)
Subscriptions() []*notification.Subscription
AddSubscription(sub *notification.Subscription) error
ReplaceSubscription(sub *notification.Subscription) error
UpdateSubscription(id int, updates map[string]any) error
RemoveSubscription(id int)
// Dispatchers
Dispatchers() []notification.DispatcherConfig
AddDispatcher(d dispatcher.Dispatcher) int
UpdateDispatcher(id int, d dispatcher.Dispatcher)
RemoveDispatcher(id int)
// Containers for preview
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
}
type Resolver struct {
HostService HostService
}
+107
View File
@@ -0,0 +1,107 @@
scalar Time
scalar Int64
scalar Any
scalar Map
# Reuse existing domain types
type Container {
id: String!
name: String!
image: String!
command: String!
created: Time!
startedAt: Time!
state: String!
health: String
host: String
labels: Map
group: String
}
type LogEvent {
id: Int!
type: String
message: Any
timestamp: Int64!
level: String
stream: String
containerId: String
}
# Notification-specific types
type NotificationRule {
id: Int!
name: String!
enabled: Boolean!
dispatcher: Dispatcher
logExpression: String!
containerExpression: String!
triggerCount: Int64!
lastTriggeredAt: Time
triggeredContainers: Int!
}
type Dispatcher {
id: Int!
name: String!
type: String!
url: String
}
type PreviewResult {
containerError: String
logError: String
matchedContainers: [Container!]!
matchedLogs: [LogEvent!]!
totalLogs: Int!
}
# Inputs
input NotificationRuleInput {
name: String!
enabled: Boolean!
dispatcherId: Int!
logExpression: String!
containerExpression: String!
}
input NotificationRuleUpdateInput {
name: String
enabled: Boolean
dispatcherId: Int
logExpression: String
containerExpression: String
}
input DispatcherInput {
name: String!
type: String!
url: String
}
input PreviewInput {
containerExpression: String!
logExpression: String
}
# Queries
type Query {
notificationRules: [NotificationRule!]!
notificationRule(id: Int!): NotificationRule
dispatchers: [Dispatcher!]!
dispatcher(id: Int!): Dispatcher
}
# Mutations
type Mutation {
createNotificationRule(input: NotificationRuleInput!): NotificationRule!
updateNotificationRule(id: Int!, input: NotificationRuleUpdateInput!): NotificationRule!
replaceNotificationRule(id: Int!, input: NotificationRuleInput!): NotificationRule!
deleteNotificationRule(id: Int!): Boolean!
createDispatcher(input: DispatcherInput!): Dispatcher!
updateDispatcher(id: Int!, input: DispatcherInput!): Dispatcher!
deleteDispatcher(id: Int!): Boolean!
previewExpression(input: PreviewInput!): PreviewResult!
}
+330
View File
@@ -0,0 +1,330 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.86
import (
"context"
"time"
"github.com/amir20/dozzle/graph/model"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)
// Labels is the resolver for the labels field.
func (r *containerResolver) Labels(ctx context.Context, obj *container.Container) (map[string]any, error) {
result := make(map[string]any)
for k, v := range obj.Labels {
result[k] = v
}
return result, nil
}
// ID is the resolver for the id field.
func (r *logEventResolver) ID(ctx context.Context, obj *container.LogEvent) (int32, error) {
return int32(obj.Id), nil
}
// Type is the resolver for the type field.
func (r *logEventResolver) Type(ctx context.Context, obj *container.LogEvent) (*string, error) {
t := string(obj.Type)
return &t, nil
}
// CreateNotificationRule is the resolver for the createNotificationRule field.
func (r *mutationResolver) CreateNotificationRule(ctx context.Context, input model.NotificationRuleInput) (*model.NotificationRule, error) {
sub := &notification.Subscription{
Name: input.Name,
Enabled: input.Enabled,
DispatcherID: int(input.DispatcherID),
LogExpression: input.LogExpression,
ContainerExpression: input.ContainerExpression,
}
if err := r.HostService.AddSubscription(sub); err != nil {
return nil, err
}
return subscriptionToNotificationRule(sub, r.HostService.Dispatchers()), nil
}
// UpdateNotificationRule is the resolver for the updateNotificationRule field.
func (r *mutationResolver) UpdateNotificationRule(ctx context.Context, id int32, input model.NotificationRuleUpdateInput) (*model.NotificationRule, error) {
updates := make(map[string]any)
if input.Name != nil {
updates["name"] = *input.Name
}
if input.Enabled != nil {
updates["enabled"] = *input.Enabled
}
if input.DispatcherID != nil {
updates["dispatcherId"] = int(*input.DispatcherID)
}
if input.LogExpression != nil {
updates["logExpression"] = *input.LogExpression
}
if input.ContainerExpression != nil {
updates["containerExpression"] = *input.ContainerExpression
}
if err := r.HostService.UpdateSubscription(int(id), updates); err != nil {
return nil, err
}
// Fetch the updated subscription
dispatchers := r.HostService.Dispatchers()
for _, sub := range r.HostService.Subscriptions() {
if sub.ID == int(id) {
return subscriptionToNotificationRule(sub, dispatchers), nil
}
}
return nil, nil
}
// ReplaceNotificationRule is the resolver for the replaceNotificationRule field.
func (r *mutationResolver) ReplaceNotificationRule(ctx context.Context, id int32, input model.NotificationRuleInput) (*model.NotificationRule, error) {
sub := &notification.Subscription{
ID: int(id),
Name: input.Name,
Enabled: input.Enabled,
DispatcherID: int(input.DispatcherID),
LogExpression: input.LogExpression,
ContainerExpression: input.ContainerExpression,
}
if err := r.HostService.ReplaceSubscription(sub); err != nil {
return nil, err
}
return subscriptionToNotificationRule(sub, r.HostService.Dispatchers()), nil
}
// DeleteNotificationRule is the resolver for the deleteNotificationRule field.
func (r *mutationResolver) DeleteNotificationRule(ctx context.Context, id int32) (bool, error) {
r.HostService.RemoveSubscription(int(id))
return true, nil
}
// CreateDispatcher is the resolver for the createDispatcher field.
func (r *mutationResolver) CreateDispatcher(ctx context.Context, input model.DispatcherInput) (*model.Dispatcher, error) {
var d dispatcher.Dispatcher
switch input.Type {
case "webhook":
url := ""
if input.URL != nil {
url = *input.URL
}
d = dispatcher.NewWebhookDispatcher(input.Name, url)
default:
return nil, &Error{Message: "unknown dispatcher type"}
}
id := r.HostService.AddDispatcher(d)
return &model.Dispatcher{
ID: int32(id),
Name: input.Name,
Type: input.Type,
URL: input.URL,
}, nil
}
// UpdateDispatcher is the resolver for the updateDispatcher field.
func (r *mutationResolver) UpdateDispatcher(ctx context.Context, id int32, input model.DispatcherInput) (*model.Dispatcher, error) {
var d dispatcher.Dispatcher
switch input.Type {
case "webhook":
url := ""
if input.URL != nil {
url = *input.URL
}
d = dispatcher.NewWebhookDispatcher(input.Name, url)
default:
return nil, &Error{Message: "unknown dispatcher type"}
}
r.HostService.UpdateDispatcher(int(id), d)
return &model.Dispatcher{
ID: id,
Name: input.Name,
Type: input.Type,
URL: input.URL,
}, nil
}
// DeleteDispatcher is the resolver for the deleteDispatcher field.
func (r *mutationResolver) DeleteDispatcher(ctx context.Context, id int32) (bool, error) {
r.HostService.RemoveDispatcher(int(id))
return true, nil
}
// PreviewExpression is the resolver for the previewExpression field.
func (r *mutationResolver) PreviewExpression(ctx context.Context, input model.PreviewInput) (*model.PreviewResult, error) {
result := &model.PreviewResult{
MatchedContainers: []*container.Container{},
MatchedLogs: []*container.LogEvent{},
}
// Compile and test container expression
var containerProgram *vm.Program
if input.ContainerExpression != "" {
program, err := expr.Compile(input.ContainerExpression, expr.Env(notification.Container{}))
if err != nil {
errStr := err.Error()
result.ContainerError = &errStr
} else {
containerProgram = program
}
}
// Compile and test log expression
var logProgram *vm.Program
if input.LogExpression != nil && *input.LogExpression != "" {
program, err := expr.Compile(*input.LogExpression, expr.Env(notification.Log{}))
if err != nil {
errStr := err.Error()
result.LogError = &errStr
} else {
logProgram = program
}
}
// If container expression is valid, find matching running containers
if containerProgram != nil {
containers, _ := r.HostService.ListAllContainers(container.ContainerLabels{})
for _, c := range containers {
if c.State != "running" {
continue
}
nc := notification.FromContainerModel(c)
evalResult, err := expr.Run(containerProgram, nc)
if err != nil {
continue
}
if match, ok := evalResult.(bool); ok && match {
containerCopy := c
result.MatchedContainers = append(result.MatchedContainers, &containerCopy)
}
}
}
// If log expression is valid and we have matching containers, fetch real logs
if logProgram != nil && len(result.MatchedContainers) > 0 {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
const maxLogs = 10
totalMatched := 0
for _, c := range result.MatchedContainers {
if len(result.MatchedLogs) >= maxLogs {
break
}
containerService, err := r.HostService.FindContainer(c.Host, c.ID, container.ContainerLabels{})
if err != nil {
continue
}
// Fetch recent logs (last 5 minutes)
from := time.Now().Add(-5 * time.Minute)
to := time.Now()
logChan, err := containerService.LogsBetweenDates(ctx, from, to, container.STDALL)
if err != nil {
continue
}
for logEvent := range logChan {
if logEvent == nil {
continue
}
// Convert to notification.Log for expression evaluation
l := notification.FromLogEvent(*logEvent)
evalResult, err := expr.Run(logProgram, l)
if err != nil {
continue
}
if match, ok := evalResult.(bool); ok && match {
totalMatched++
if len(result.MatchedLogs) < maxLogs {
result.MatchedLogs = append(result.MatchedLogs, logEvent)
}
}
}
}
result.TotalLogs = int32(totalMatched)
}
return result, nil
}
// NotificationRules is the resolver for the notificationRules field.
func (r *queryResolver) NotificationRules(ctx context.Context) ([]*model.NotificationRule, error) {
subscriptions := r.HostService.Subscriptions()
dispatchers := r.HostService.Dispatchers()
rules := make([]*model.NotificationRule, len(subscriptions))
for i, sub := range subscriptions {
rules[i] = subscriptionToNotificationRule(sub, dispatchers)
}
return rules, nil
}
// NotificationRule is the resolver for the notificationRule field.
func (r *queryResolver) NotificationRule(ctx context.Context, id int32) (*model.NotificationRule, error) {
dispatchers := r.HostService.Dispatchers()
for _, sub := range r.HostService.Subscriptions() {
if sub.ID == int(id) {
return subscriptionToNotificationRule(sub, dispatchers), nil
}
}
return nil, nil
}
// Dispatchers is the resolver for the dispatchers field.
func (r *queryResolver) Dispatchers(ctx context.Context) ([]*model.Dispatcher, error) {
dispatchers := r.HostService.Dispatchers()
result := make([]*model.Dispatcher, len(dispatchers))
for i, d := range dispatchers {
result[i] = dispatcherConfigToDispatcher(&d)
}
return result, nil
}
// Dispatcher is the resolver for the dispatcher field.
func (r *queryResolver) Dispatcher(ctx context.Context, id int32) (*model.Dispatcher, error) {
for _, d := range r.HostService.Dispatchers() {
if d.ID == int(id) {
return dispatcherConfigToDispatcher(&d), nil
}
}
return nil, nil
}
// Container returns ContainerResolver implementation.
func (r *Resolver) Container() ContainerResolver { return &containerResolver{r} }
// LogEvent returns LogEventResolver implementation.
func (r *Resolver) LogEvent() LogEventResolver { return &logEventResolver{r} }
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type containerResolver struct{ *Resolver }
type logEventResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.3
// protoc v6.33.4
// source: rpc.proto
package pb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.3
// - protoc v6.33.4
// source: rpc.proto
package pb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.3
// protoc v6.33.4
// source: types.proto
package pb
@@ -0,0 +1,11 @@
package dispatcher
import (
"context"
)
// Dispatcher is responsible for sending notifications to external systems
type Dispatcher interface {
// Send sends a notification to the configured destination
Send(ctx context.Context, notification any) error
}
@@ -0,0 +1,55 @@
package dispatcher
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// WebhookDispatcher sends notifications to a webhook URL
type WebhookDispatcher struct {
Name string
URL string
client *http.Client
}
// NewWebhookDispatcher creates a new webhook dispatcher
func NewWebhookDispatcher(name, url string) *WebhookDispatcher {
return &WebhookDispatcher{
Name: name,
URL: url,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// Send sends a notification to the webhook URL
func (w *WebhookDispatcher) Send(ctx context.Context, notification any) error {
payload, err := json.Marshal(notification)
if err != nil {
return fmt.Errorf("failed to marshal notification: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.URL, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := w.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned non-success status code: %d", resp.StatusCode)
}
return nil
}
+188
View File
@@ -0,0 +1,188 @@
package notification
import (
"context"
"fmt"
"time"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
)
// ContainerMatcher is an interface for checking if a container should be listened to
type ContainerMatcher interface {
ShouldListenToContainer(c container.Container) bool
}
// ContainerLogListener manages active log streams for containers across multiple clients
type ContainerLogListener struct {
clients []container_support.ClientService
containerClients *xsync.Map[string, container_support.ClientService] // containerID -> owning client
activeStreams *xsync.Map[string, context.CancelFunc] // containerID -> cancel function
matcher ContainerMatcher
logChannel chan *container.LogEvent
ctx context.Context
}
// NewContainerLogListener creates a new listener for multiple clients
func NewContainerLogListener(ctx context.Context, clients []container_support.ClientService) *ContainerLogListener {
return &ContainerLogListener{
clients: clients,
containerClients: xsync.NewMap[string, container_support.ClientService](),
activeStreams: xsync.NewMap[string, context.CancelFunc](),
logChannel: make(chan *container.LogEvent, 1000),
ctx: ctx,
}
}
// Start begins listening for container events and processes log streams
func (l *ContainerLogListener) Start(matcher ContainerMatcher) error {
l.matcher = matcher
// Subscribe to new containers from all clients
containerChan := make(chan container.Container, 10)
// Get all current containers from all clients
for _, client := range l.clients {
containers, err := client.ListContainers(l.ctx, nil)
if err != nil {
log.Warn().Err(err).Msg("Failed to list containers from client")
continue
}
// Start listening to containers that match
for _, c := range containers {
if l.matcher.ShouldListenToContainer(c) {
l.startListening(c, client)
}
}
// Subscribe to new containers from this client
client.SubscribeContainersStarted(l.ctx, containerChan)
}
go func() {
for {
select {
case <-l.ctx.Done():
return
case c := <-containerChan:
if l.matcher.ShouldListenToContainer(c) {
l.startListeningByID(c)
}
}
}
}()
return nil
}
// UpdateStreams updates which containers to listen to based on current matcher rules
func (l *ContainerLogListener) UpdateStreams() error {
// Get all current containers from all clients
for _, client := range l.clients {
containers, err := client.ListContainers(l.ctx, nil)
if err != nil {
log.Warn().Err(err).Msg("Failed to list containers from client")
continue
}
// Check each container against matcher
for _, c := range containers {
shouldListen := l.matcher.ShouldListenToContainer(c)
isListening := l.isListening(c.ID)
if shouldListen && !isListening {
l.startListening(c, client)
} else if !shouldListen && isListening {
l.stopListening(c.ID)
}
}
}
return nil
}
// startListening starts listening to a container's logs with a known client
func (l *ContainerLogListener) startListening(c container.Container, client container_support.ClientService) {
streamCtx, cancel := context.WithCancel(l.ctx)
// Only store if not already present
_, loaded := l.activeStreams.LoadOrStore(c.ID, cancel)
if loaded {
cancel() // Already listening, cancel the new context
return
}
l.containerClients.Store(c.ID, client)
go func() {
log.Info().Str("containerID", c.ID).Str("name", c.Name).Msg("Started listening to container")
if err := client.StreamLogs(streamCtx, c, time.Now(), container.STDALL, l.logChannel); err != nil {
log.Error().Err(err).Str("containerID", c.ID).Msg("Error streaming logs")
}
}()
}
// startListeningByID finds the client for a container and starts listening
func (l *ContainerLogListener) startListeningByID(c container.Container) {
for _, client := range l.clients {
containers, err := client.ListContainers(l.ctx, nil)
if err != nil {
continue
}
for _, container := range containers {
if container.ID == c.ID {
l.startListening(c, client)
return
}
}
}
log.Warn().Str("containerID", c.ID).Msg("Could not find client for container")
}
// stopListening stops listening to a container's logs
func (l *ContainerLogListener) stopListening(containerID string) {
if cancel, exists := l.activeStreams.LoadAndDelete(containerID); exists {
cancel()
l.containerClients.Delete(containerID)
log.Info().Str("containerID", containerID).Msg("Stopped listening to container")
}
}
// isListening returns true if listening to a container
func (l *ContainerLogListener) isListening(containerID string) bool {
_, exists := l.activeStreams.Load(containerID)
return exists
}
// FindContainer finds a container by ID using the client that owns it
func (l *ContainerLogListener) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
client, exists := l.containerClients.Load(id)
if !exists {
return container.Container{}, fmt.Errorf("container %s not found in any client", id)
}
return client.FindContainer(ctx, id, labels)
}
// LogChannel returns the channel for log events
func (l *ContainerLogListener) LogChannel() <-chan *container.LogEvent {
return l.logChannel
}
// ListContainers returns all containers from all clients
func (l *ContainerLogListener) ListContainers() []container.Container {
var result []container.Container
for _, client := range l.clients {
containers, err := client.ListContainers(l.ctx, nil)
if err != nil {
log.Warn().Err(err).Msg("Failed to list containers from client")
continue
}
result = append(result, containers...)
}
return result
}
+471
View File
@@ -0,0 +1,471 @@
package notification
import (
"context"
"fmt"
"io"
"slices"
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/notification/dispatcher"
"github.com/expr-lang/expr"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"go.yaml.in/yaml/v3"
)
// Manager manages notification subscriptions and dispatches notifications
type Manager struct {
subscriptions *xsync.Map[int, *Subscription]
dispatchers *xsync.Map[int, dispatcher.Dispatcher]
subscriptionCounter atomic.Int32
dispatcherCounter atomic.Int32
listener *ContainerLogListener
ctx context.Context
cancel context.CancelFunc
}
// NewManager creates a new notification manager
func NewManager(listener *ContainerLogListener) *Manager {
ctx, cancel := context.WithCancel(context.Background())
m := &Manager{
subscriptions: xsync.NewMap[int, *Subscription](),
dispatchers: xsync.NewMap[int, dispatcher.Dispatcher](),
listener: listener,
ctx: ctx,
cancel: cancel,
}
// Start processing log events from the listener
go m.processLogEvents()
return m
}
// Start initializes the manager and starts the log listener
func (m *Manager) Start() error {
if m.listener != nil {
return m.listener.Start(m)
}
return nil
}
// ShouldListenToContainer implements ContainerMatcher interface
func (m *Manager) ShouldListenToContainer(c container.Container) bool {
notificationContainer := FromContainerModel(c)
shouldListen := false
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
if sub.Enabled && sub.MatchesContainer(notificationContainer) {
shouldListen = true
return false
}
return true
})
return shouldListen
}
// AddSubscription adds a new subscription with compiled expressions
func (m *Manager) AddSubscription(sub *Subscription) error {
// Auto-increment ID using atomic counter
sub.ID = int(m.subscriptionCounter.Add(1))
sub.Enabled = true
// Compile container expression if provided
if sub.ContainerExpression != "" {
program, err := expr.Compile(sub.ContainerExpression, expr.Env(Container{}))
if err != nil {
return fmt.Errorf("failed to compile container expression: %w", err)
}
sub.ContainerProgram = program
}
// Compile log expression if provided
if sub.LogExpression != "" {
program, err := expr.Compile(sub.LogExpression, expr.Env(Log{}))
if err != nil {
return fmt.Errorf("failed to compile log expression: %w", err)
}
sub.LogProgram = program
}
m.subscriptions.Store(sub.ID, sub)
log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Added subscription")
// Update listener to start/stop streams based on new subscription
if m.listener != nil {
if err := m.listener.UpdateStreams(); err != nil {
log.Error().Err(err).Msg("Failed to update listener streams")
}
}
return nil
}
// RemoveSubscription removes a subscription by ID
func (m *Manager) RemoveSubscription(id int) {
if sub, ok := m.subscriptions.LoadAndDelete(id); ok {
log.Info().Int("id", id).Str("name", sub.Name).Msg("Removed subscription")
// Update listener to stop streams that are no longer needed
if m.listener != nil {
if err := m.listener.UpdateStreams(); err != nil {
log.Error().Err(err).Msg("Failed to update listener streams")
}
}
}
}
// ReplaceSubscription replaces a subscription with new data
func (m *Manager) ReplaceSubscription(sub *Subscription) error {
// Compile container expression if provided
if sub.ContainerExpression != "" {
program, err := expr.Compile(sub.ContainerExpression, expr.Env(Container{}))
if err != nil {
return fmt.Errorf("failed to compile container expression: %w", err)
}
sub.ContainerProgram = program
}
// Compile log expression if provided
if sub.LogExpression != "" {
program, err := expr.Compile(sub.LogExpression, expr.Env(Log{}))
if err != nil {
return fmt.Errorf("failed to compile log expression: %w", err)
}
sub.LogProgram = program
}
// Preserve enabled state from existing subscription if it exists
if existing, ok := m.subscriptions.Load(sub.ID); ok {
sub.Enabled = existing.Enabled
} else {
sub.Enabled = true
}
m.subscriptions.Store(sub.ID, sub)
log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Replaced subscription")
// Update listener to start/stop streams based on new subscription
if m.listener != nil {
if err := m.listener.UpdateStreams(); err != nil {
log.Error().Err(err).Msg("Failed to update listener streams")
}
}
return nil
}
// UpdateSubscription updates a subscription with the provided fields
func (m *Manager) UpdateSubscription(id int, updates map[string]any) error {
var updateErr error
_, ok := m.subscriptions.Compute(id, func(sub *Subscription, loaded bool) (*Subscription, xsync.ComputeOp) {
if !loaded {
updateErr = fmt.Errorf("subscription not found")
return nil, xsync.CancelOp
}
// Clone the subscription
updated := &Subscription{
ID: sub.ID,
Name: sub.Name,
Enabled: sub.Enabled,
DispatcherID: sub.DispatcherID,
ContainerExpression: sub.ContainerExpression,
ContainerProgram: sub.ContainerProgram,
LogExpression: sub.LogExpression,
LogProgram: sub.LogProgram,
}
// Apply updates to the clone
for key, value := range updates {
switch key {
case "name":
if name, ok := value.(string); ok {
updated.Name = name
}
case "enabled":
if enabled, ok := value.(bool); ok {
updated.Enabled = enabled
}
case "dispatcherId":
if dispatcherID, ok := value.(int); ok {
updated.DispatcherID = dispatcherID
}
case "containerExpression":
if exprStr, ok := value.(string); ok {
program, err := expr.Compile(exprStr, expr.Env(Container{}))
if err != nil {
updateErr = fmt.Errorf("failed to compile container expression: %w", err)
return nil, xsync.CancelOp
}
updated.ContainerExpression = exprStr
updated.ContainerProgram = program
}
case "logExpression":
if exprStr, ok := value.(string); ok {
if exprStr != "" {
program, err := expr.Compile(exprStr, expr.Env(Log{}))
if err != nil {
updateErr = fmt.Errorf("failed to compile log expression: %w", err)
return nil, xsync.CancelOp
}
updated.LogExpression = exprStr
updated.LogProgram = program
}
}
}
}
return updated, xsync.UpdateOp
})
if updateErr != nil {
return updateErr
}
if !ok {
return fmt.Errorf("subscription not found")
}
log.Debug().Int("id", id).Interface("updates", updates).Msg("Updated subscription")
// Update listener streams in case expressions changed
if m.listener != nil {
if err := m.listener.UpdateStreams(); err != nil {
log.Error().Err(err).Msg("Failed to update listener streams")
}
}
return nil
}
// AddDispatcher adds a dispatcher and returns its auto-generated ID
func (m *Manager) AddDispatcher(d dispatcher.Dispatcher) int {
id := int(m.dispatcherCounter.Add(1))
m.dispatchers.Store(id, d)
log.Info().Int("id", id).Msg("Added dispatcher")
return id
}
// UpdateDispatcher updates a dispatcher by ID
func (m *Manager) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
m.dispatchers.Store(id, d)
log.Info().Int("id", id).Msg("Updated dispatcher")
}
// RemoveDispatcher removes a dispatcher by ID
func (m *Manager) RemoveDispatcher(id int) {
if _, ok := m.dispatchers.LoadAndDelete(id); ok {
log.Info().Int("id", id).Msg("Removed dispatcher")
}
}
// Subscriptions returns all subscriptions sorted by ID
func (m *Manager) Subscriptions() []*Subscription {
result := make([]*Subscription, 0)
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
result = append(result, sub)
return true
})
slices.SortFunc(result, func(a, b *Subscription) int {
return a.ID - b.ID
})
return result
}
// Dispatchers returns all dispatchers as DispatcherConfig sorted by ID
func (m *Manager) Dispatchers() []DispatcherConfig {
result := make([]DispatcherConfig, 0)
m.dispatchers.Range(func(id int, d dispatcher.Dispatcher) bool {
switch v := d.(type) {
case *dispatcher.WebhookDispatcher:
result = append(result, DispatcherConfig{
ID: id,
Name: v.Name,
Type: "webhook",
URL: v.URL,
})
}
return true
})
slices.SortFunc(result, func(a, b DispatcherConfig) int {
return a.ID - b.ID
})
return result
}
// processLogEvents processes log events from the listener channel
func (m *Manager) processLogEvents() {
for {
select {
case <-m.ctx.Done():
return
case logEvent := <-m.listener.LogChannel():
if logEvent == nil {
return
}
m.processLogEvent(logEvent)
}
}
}
// processLogEvent processes a single log event and sends notifications for matching subscriptions
func (m *Manager) processLogEvent(logEvent *container.LogEvent) {
// Get container from log event's ContainerID
ctx, cancel := context.WithTimeout(m.ctx, 5*time.Second)
defer cancel()
c, err := m.listener.FindContainer(ctx, logEvent.ContainerID, nil)
if err != nil {
log.Error().Err(err).Str("containerID", logEvent.ContainerID).Msg("Failed to find container")
return
}
notificationContainer := FromContainerModel(c)
notificationLog := FromLogEvent(*logEvent)
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
// Skip disabled subscriptions
if !sub.Enabled {
return true
}
// Check container filter
if !sub.MatchesContainer(notificationContainer) {
return true
}
sub.AddTriggeredContainer(notificationContainer.ID)
// Check log filter
if !sub.MatchesLog(notificationLog) {
return true
}
// Update stats
sub.TriggerCount.Add(1)
now := time.Now()
sub.LastTriggeredAt.Store(&now)
log.Debug().Str("containerID", notificationContainer.ID).Interface("log", notificationLog.Message).Msg("Matched subscription")
// Create notification
notification := Notification{
ID: fmt.Sprintf("%s-%d", c.ID, time.Now().UnixNano()),
Container: notificationContainer,
Log: notificationLog,
Timestamp: time.Now(),
}
// Send to the subscription's dispatcher
if d, ok := m.dispatchers.Load(sub.DispatcherID); ok {
go m.sendNotification(d, notification, sub.DispatcherID)
}
return true
})
}
// sendNotification sends a notification using the dispatcher
func (m *Manager) sendNotification(d dispatcher.Dispatcher, notification Notification, id int) {
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
defer cancel()
if err := d.Send(ctx, notification); err != nil {
log.Error().Err(err).Int("subscription", id).Msg("Failed to send notification")
}
}
// WriteConfig writes the current configuration to a writer in YAML format
func (m *Manager) WriteConfig(w io.Writer) error {
config := Config{
Subscriptions: m.Subscriptions(),
Dispatchers: m.Dispatchers(),
}
encoder := yaml.NewEncoder(w)
defer encoder.Close()
return encoder.Encode(config)
}
// LoadConfig reads configuration from a reader in YAML format and loads it
func (m *Manager) LoadConfig(r io.Reader) error {
var config Config
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(&config); err != nil {
return fmt.Errorf("failed to decode config: %w", err)
}
// Find max IDs to initialize counters
var maxSubID, maxDispatcherID int
for _, sub := range config.Subscriptions {
if sub.ID > maxSubID {
maxSubID = sub.ID
}
}
for _, d := range config.Dispatchers {
if d.ID > maxDispatcherID {
maxDispatcherID = d.ID
}
}
m.subscriptionCounter.Store(int32(maxSubID))
m.dispatcherCounter.Store(int32(maxDispatcherID))
// Load subscriptions
for _, sub := range config.Subscriptions {
if err := m.loadSubscription(sub); err != nil {
return fmt.Errorf("failed to add subscription %s: %w", sub.Name, err)
}
}
// Load dispatchers
for _, dispatcherConfig := range config.Dispatchers {
var d dispatcher.Dispatcher
switch dispatcherConfig.Type {
case "webhook":
d = dispatcher.NewWebhookDispatcher(dispatcherConfig.Name, dispatcherConfig.URL)
default:
return fmt.Errorf("unknown dispatcher type: %s", dispatcherConfig.Type)
}
m.dispatchers.Store(dispatcherConfig.ID, d)
log.Info().Int("id", dispatcherConfig.ID).Msg("Loaded dispatcher")
}
// Update listener to start streams for loaded subscriptions
if m.listener != nil {
if err := m.listener.UpdateStreams(); err != nil {
return fmt.Errorf("failed to update listener streams: %w", err)
}
}
return nil
}
// loadSubscription loads a subscription with its existing ID (used when loading from config)
func (m *Manager) loadSubscription(sub *Subscription) error {
// Compile container expression if provided
if sub.ContainerExpression != "" {
program, err := expr.Compile(sub.ContainerExpression, expr.Env(Container{}))
if err != nil {
return fmt.Errorf("failed to compile container expression: %w", err)
}
sub.ContainerProgram = program
}
// Compile log expression if provided
if sub.LogExpression != "" {
program, err := expr.Compile(sub.LogExpression, expr.Env(Log{}))
if err != nil {
return fmt.Errorf("failed to compile log expression: %w", err)
}
sub.LogProgram = program
}
m.subscriptions.Store(sub.ID, sub)
log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Loaded subscription")
return nil
}
+167
View File
@@ -0,0 +1,167 @@
package notification
import (
"strings"
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/puzpuzpuz/xsync/v4"
)
// Notification represents a notification event that can be filtered and sent
type Notification struct {
ID string `json:"id"`
Container Container `json:"container"`
Log Log `json:"log"`
Timestamp time.Time `json:"timestamp"`
}
// Container represents a simplified container structure optimized for expr filtering
type Container struct {
ID string `json:"id" expr:"id"`
Name string `json:"name" expr:"name"`
Image string `json:"image" expr:"image"`
State string `json:"state" expr:"state"`
Health string `json:"health" expr:"health"`
Host string `json:"host" expr:"host"`
Labels map[string]string `json:"labels" expr:"labels"`
}
// FromContainerModel converts internal container.Container to notification.Container
func FromContainerModel(c container.Container) Container {
return Container{
ID: c.ID,
Name: c.Name,
Image: c.Image,
State: c.State,
Health: c.Health,
Host: c.Host,
Labels: c.Labels,
}
}
// Log represents a log entry with message that can be string or object
type Log struct {
ID uint32 `json:"id" expr:"id"`
Message any `json:"message" expr:"message"` // string for simple/grouped logs, map for complex logs
Timestamp int64 `json:"timestamp" expr:"timestamp"`
Level string `json:"level" expr:"level"`
Stream string `json:"stream" expr:"stream"`
Type string `json:"type" expr:"type"`
}
// FromLogEvent converts container.LogEvent to notification.Log
func FromLogEvent(l container.LogEvent) Log {
message := extractMessage(l)
return Log{
ID: l.Id,
Message: message,
Timestamp: l.Timestamp,
Level: l.Level,
Stream: l.Stream,
Type: string(l.Type),
}
}
// extractMessage extracts and joins message from LogEvent
// For grouped logs (fragments), joins them into a single string
// For complex logs (JSON/objects), keeps the original map
// For simple logs, returns the string as-is
func extractMessage(l container.LogEvent) any {
switch v := l.Message.(type) {
case string:
return v
case []container.LogFragment:
var parts []string
for _, fragment := range v {
parts = append(parts, fragment.Message)
}
return strings.Join(parts, "")
default:
// For complex objects (maps/JSON), keep the original structure
return v
}
}
// Subscription represents a subscription to log streams with filtering
type Subscription struct {
ID int `json:"id" yaml:"id"`
Name string `json:"name" yaml:"name"`
Enabled bool `json:"enabled" yaml:"enabled"`
DispatcherID int `json:"dispatcherId" yaml:"dispatcherId"`
LogExpression string `json:"logExpression" yaml:"logExpression"`
ContainerExpression string `json:"containerExpression" yaml:"containerExpression"`
// Compiled log filter expression
LogProgram *vm.Program `json:"-" yaml:"-"` // Compiled log filter expression
ContainerProgram *vm.Program `json:"-" yaml:"-"` // Compiled container filter expression
// Runtime stats (not persisted)
TriggerCount atomic.Int64 `json:"-" yaml:"-"`
LastTriggeredAt atomic.Pointer[time.Time] `json:"-" yaml:"-"`
TriggeredContainerIDs *xsync.Map[string, struct{}] `json:"-" yaml:"-"` // unique container IDs that triggered
}
// TriggeredContainersCount returns the number of unique containers that triggered this subscription
func (s *Subscription) TriggeredContainersCount() int {
if s.TriggeredContainerIDs == nil {
return 0
}
return s.TriggeredContainerIDs.Size()
}
// AddTriggeredContainer adds a container ID to the triggered set
func (s *Subscription) AddTriggeredContainer(id string) {
if s.TriggeredContainerIDs == nil {
s.TriggeredContainerIDs = xsync.NewMap[string, struct{}]()
}
s.TriggeredContainerIDs.Store(id, struct{}{})
}
// DispatcherConfig represents a dispatcher configuration
type DispatcherConfig struct {
ID int `json:"id" yaml:"id"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"` // "webhook", etc.
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
// Config represents the persisted notification configuration
type Config struct {
Subscriptions []*Subscription `json:"subscriptions" yaml:"subscriptions"`
Dispatchers []DispatcherConfig `json:"dispatchers" yaml:"dispatchers"`
}
// MatchesContainer checks if a container matches this subscription's container filter
func (s *Subscription) MatchesContainer(c Container) bool {
if s.ContainerProgram == nil {
return false
}
result, err := expr.Run(s.ContainerProgram, c)
if err != nil {
return false
}
match, ok := result.(bool)
return ok && match
}
// MatchesLog checks if a log matches this subscription's log filter
func (s *Subscription) MatchesLog(l Log) bool {
if s.LogProgram == nil {
return false
}
result, err := expr.Run(s.LogProgram, l)
if err != nil {
return false
}
match, ok := result.(bool)
return ok && match
}
+125 -2
View File
@@ -3,9 +3,12 @@ package docker_support
import (
"context"
"fmt"
"os"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/rs/zerolog/log"
lop "github.com/samber/lo/parallel"
@@ -27,11 +30,13 @@ type ClientManager interface {
Subscribe(ctx context.Context, channel chan<- container.Host)
Hosts(ctx context.Context) []container.Host
LocalClients() []container.Client
LocalClientServices() []container_support.ClientService
}
type MultiHostService struct {
manager ClientManager
timeout time.Duration
manager ClientManager
timeout time.Duration
notificationManager *notification.Manager
}
func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHostService {
@@ -168,6 +173,124 @@ func (m *MultiHostService) LocalClients() []container.Client {
return m.manager.LocalClients()
}
func (m *MultiHostService) LocalClientServices() []container_support.ClientService {
return m.manager.LocalClientServices()
}
func (m *MultiHostService) TotalClients() int {
return len(m.manager.List())
}
const notificationConfigPath = "./data/notifications.yml"
// StartNotificationManager initializes and starts the notification manager
func (m *MultiHostService) StartNotificationManager(ctx context.Context) error {
clients := m.manager.LocalClientServices()
listener := notification.NewContainerLogListener(ctx, clients)
m.notificationManager = notification.NewManager(listener)
// Start first so matcher is available for LoadConfig
if err := m.notificationManager.Start(); err != nil {
return err
}
// Load config if exists
if file, err := os.Open(notificationConfigPath); err == nil {
defer file.Close()
if err := m.notificationManager.LoadConfig(file); err != nil {
log.Warn().Err(err).Msg("Could not load notification config")
} else {
log.Debug().Str("path", notificationConfigPath).Msg("Loaded notification config")
}
}
return nil
}
func (m *MultiHostService) saveNotificationConfig() {
if err := os.MkdirAll("./data", 0755); err != nil {
log.Error().Err(err).Msg("Could not create data directory")
return
}
file, err := os.Create(notificationConfigPath)
if err != nil {
log.Error().Err(err).Msg("Could not create notification config file")
return
}
defer file.Close()
if err := m.notificationManager.WriteConfig(file); err != nil {
log.Error().Err(err).Msg("Could not write notification config")
}
}
// AddSubscription adds a subscription to local manager and broadcasts to agents
func (m *MultiHostService) AddSubscription(sub *notification.Subscription) error {
// Add to local manager
if err := m.notificationManager.AddSubscription(sub); err != nil {
return err
}
// TODO: Broadcast to agents via gRPC when agent notification support is added
m.saveNotificationConfig()
return nil
}
// RemoveSubscription removes a subscription from local manager and broadcasts to agents
func (m *MultiHostService) RemoveSubscription(id int) {
// Remove from local manager
m.notificationManager.RemoveSubscription(id)
// TODO: Broadcast to agents via gRPC when agent notification support is added
m.saveNotificationConfig()
}
// AddDispatcher adds a dispatcher and returns its auto-generated ID
func (m *MultiHostService) AddDispatcher(d dispatcher.Dispatcher) int {
id := m.notificationManager.AddDispatcher(d)
m.saveNotificationConfig()
return id
}
// UpdateDispatcher updates a dispatcher by ID
func (m *MultiHostService) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
m.notificationManager.UpdateDispatcher(id, d)
m.saveNotificationConfig()
}
// RemoveDispatcher removes a dispatcher by ID
func (m *MultiHostService) RemoveDispatcher(id int) {
m.notificationManager.RemoveDispatcher(id)
m.saveNotificationConfig()
}
// ReplaceSubscription replaces a subscription with new data
func (m *MultiHostService) ReplaceSubscription(sub *notification.Subscription) error {
if err := m.notificationManager.ReplaceSubscription(sub); err != nil {
return err
}
m.saveNotificationConfig()
return nil
}
// UpdateSubscription updates a subscription with the provided fields
func (m *MultiHostService) UpdateSubscription(id int, updates map[string]any) error {
if err := m.notificationManager.UpdateSubscription(id, updates); err != nil {
return err
}
m.saveNotificationConfig()
return nil
}
// Subscriptions returns all subscriptions
func (m *MultiHostService) Subscriptions() []*notification.Subscription {
return m.notificationManager.Subscriptions()
}
// Dispatchers returns all dispatchers
func (m *MultiHostService) Dispatchers() []notification.DispatcherConfig {
return m.notificationManager.Dispatchers()
}
@@ -195,3 +195,17 @@ func (m *RetriableClientManager) LocalClients() []container.Client {
return clients
}
func (m *RetriableClientManager) LocalClientServices() []container_support.ClientService {
services := m.List()
result := make([]container_support.ClientService, 0)
for _, service := range services {
if _, ok := service.(*DockerClientService); ok {
result = append(result, service)
}
}
return result
}
@@ -236,3 +236,17 @@ func (m *SwarmClientManager) String() string {
func (m *SwarmClientManager) LocalClients() []container.Client {
return []container.Client{m.localClient}
}
func (m *SwarmClientManager) LocalClientServices() []container_support.ClientService {
m.mu.RLock()
defer m.mu.RUnlock()
result := make([]container_support.ClientService, 0)
for _, service := range m.clients {
if _, ok := service.(*DockerClientService); ok {
result = append(result, service)
}
}
return result
}
@@ -7,6 +7,8 @@ import (
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/k8s"
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -131,3 +133,43 @@ func (m *K8sClusterService) SubscribeAvailableHosts(ctx context.Context, hosts c
func (m *K8sClusterService) LocalClients() []container.Client {
return []container.Client{m.client.client}
}
func (m *K8sClusterService) LocalClientServices() []container_support.ClientService {
return []container_support.ClientService{m.client}
}
// Notification methods - not yet implemented for k8s
func (m *K8sClusterService) AddSubscription(sub *notification.Subscription) error {
// TODO Implement notification subscription for k8s mode
return fmt.Errorf("notifications not supported in k8s mode")
}
func (m *K8sClusterService) RemoveSubscription(id int) {
}
func (m *K8sClusterService) ReplaceSubscription(sub *notification.Subscription) error {
return fmt.Errorf("notifications not supported in k8s mode")
}
func (m *K8sClusterService) UpdateSubscription(id int, updates map[string]any) error {
return fmt.Errorf("notifications not supported in k8s mode")
}
func (m *K8sClusterService) Subscriptions() []*notification.Subscription {
return []*notification.Subscription{}
}
func (m *K8sClusterService) AddDispatcher(d dispatcher.Dispatcher) int {
return 0
}
func (m *K8sClusterService) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
}
func (m *K8sClusterService) RemoveDispatcher(id int) {
}
func (m *K8sClusterService) Dispatchers() []notification.DispatcherConfig {
return []notification.DispatcherConfig{}
}
+28
View File
@@ -0,0 +1,28 @@
package web
import (
"net/http"
gqlhandler "github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/amir20/dozzle/graph"
)
func (h *handler) graphqlHandler() http.Handler {
srv := gqlhandler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{
Resolvers: &graph.Resolver{
HostService: h.hostService,
},
}))
return srv
}
func (h *handler) graphqlPlaygroundHandler() http.Handler {
endpoint := h.config.Base + "/api/graphql"
// Avoid double slashes when base is "/"
if h.config.Base == "/" {
endpoint = "/api/graphql"
}
return playground.Handler("Dozzle GraphQL", endpoint)
}
+24
View File
@@ -10,6 +10,8 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/go-chi/chi/v5"
@@ -72,6 +74,17 @@ type HostService interface {
LocalHost() (container.Host, error)
SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host)
LocalClients() []container.Client
LocalClientServices() []container_support.ClientService
// Notification methods
AddSubscription(sub *notification.Subscription) error
RemoveSubscription(id int)
ReplaceSubscription(sub *notification.Subscription) error
UpdateSubscription(id int, updates map[string]any) error
Subscriptions() []*notification.Subscription
AddDispatcher(d dispatcher.Dispatcher) int
UpdateDispatcher(id int, d dispatcher.Dispatcher)
RemoveDispatcher(id int)
Dispatchers() []notification.DispatcherConfig
}
type handler struct {
@@ -116,6 +129,8 @@ func createRouter(h *handler) *chi.Mux {
if h.config.Authorization.Provider != NONE {
r.Use(auth.RequireAuthentication)
}
// Log streams
r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs)
r.Get("/hosts/{host}/logs/stream", h.streamHostLogs)
r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates)
@@ -124,6 +139,8 @@ func createRouter(h *handler) *chi.Mux {
r.Get("/labels/{labels}/logs/stream", h.streamLogsWithLabels)
r.Get("/groups/{group}/logs/stream", h.streamGroupedLogs)
r.Get("/events/stream", h.streamEvents)
// Action
if h.config.EnableActions {
r.Post("/hosts/{host}/containers/{id}/actions/{action}", h.containerActions)
}
@@ -131,6 +148,7 @@ func createRouter(h *handler) *chi.Mux {
r.Get("/hosts/{host}/containers/{id}/attach", h.attach)
r.Get("/hosts/{host}/containers/{id}/exec", h.exec)
}
r.Get("/releases", h.releases)
if !h.config.DisableAvatars {
r.Get("/profile/avatar", h.avatar)
@@ -140,6 +158,12 @@ func createRouter(h *handler) *chi.Mux {
if log.Debug().Enabled() {
r.Get("/debug/store", h.debugStore)
}
// GraphQL endpoint
r.Handle("/graphql", h.graphqlHandler())
if h.config.Dev {
r.Handle("/graphql/playground", h.graphqlPlaygroundHandler())
}
})
// Public API routes
+65
View File
@@ -80,6 +80,7 @@ title:
login: Authentication Required
dashboard: 1 container | {count} containers
settings: Settings
notifications: Notifications
button:
logout: Logout
login: Login
@@ -163,3 +164,67 @@ analytics:
evaluating_query: Evaluating query...
total_records: Total {count} records.
showing_first: Showing first {count}.
notifications:
title: Notifications
description: Configure where and when to receive alerts
destinations: Destinations
add-destination: Add destination
alerts: Alerts
add: Add
filter:
all: All ({count})
enabled: Enabled ({count})
paused: Paused ({count})
no-alerts: No alerts configured yet. Create one to get started.
alert:
paused: Paused
containers: Containers
log-filter: Log filter
destination: Destination
dispatcher-deleted: Dispatcher deleted
containers-count: "{count} containers"
triggered-count: "{count} triggered"
last-triggered: "Last: {time}"
destination:
http-webhook: HTTP Webhook
dozzle-cloud: Dozzle Cloud
edit: Edit
delete: Delete
alert-form:
create-title: Create Alert
edit-title: Edit Alert
description: Subscribe to log events matching your criteria
alert-name: Alert Name
alert-name-placeholder: e.g., Test API Errors
container-filter: Container Filter
container-filter-placeholder: name contains "api"
containers-match: "{count} containers match: {names}"
no-containers-match: No containers match this filter
log-filter: Log Filter
log-filter-placeholder: level == "error" && message contains "timeout"
logs-match: "{count} logs match"
no-logs-match: No logs match this filter
destination: Destination
select-destination: Select a destination
no-destinations: No destinations configured. Add one first.
preview: Preview
cancel: Cancel
save: Save
create: Create Alert
destination-form:
create-title: Add Destination
edit-title: Edit Destination
description: Where should notifications be sent?
name: Name
name-placeholder: e.g., Production Slack
type: Type
webhook-title: HTTP Webhook
webhook-description: Slack, Discord, custom endpoint
cloud-title: Dozzle Cloud
cloud-description: Push, email, and dashboard
webhook-url: Webhook URL
webhook-url-placeholder: https://hooks.foo.com/services/...
test: Test
cancel: Cancel
save: Save
add: Add Destination
+11 -4
View File
@@ -53,15 +53,20 @@ func main() {
log.Info().Msgf("Dozzle version %s", args.Version())
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var hostService web.HostService
if args.Mode == "server" {
multiHostService := cli.CreateMultiHostService(certs, args)
if multiHostService.TotalClients() == 0 {
log.Fatal().Msg("Could not connect to any Docker Engine")
} else {
log.Info().Int("clients", multiHostService.TotalClients()).Msg("Connected to Docker")
}
if err := multiHostService.StartNotificationManager(ctx); err != nil {
log.Fatal().Err(err).Msg("Could not start notification manager")
}
hostService = multiHostService
} else if args.Mode == "swarm" {
localClient, err := docker.NewLocalClient("")
@@ -74,7 +79,11 @@ func main() {
}
agentManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs)
manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout, agentManager, args.Filter)
hostService = docker_support.NewMultiHostService(manager, args.Timeout)
multiHostService := docker_support.NewMultiHostService(manager, args.Timeout)
if err := multiHostService.StartNotificationManager(ctx); err != nil {
log.Fatal().Err(err).Msg("Could not start notification manager")
}
hostService = multiHostService
log.Info().Msg("Starting in swarm mode")
listener, err := net.Listen("tcp", ":7007")
if err != nil {
@@ -115,8 +124,6 @@ func main() {
log.Fatal().Err(err).Msg("failed to listen")
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
stop()
+17
View File
@@ -24,11 +24,18 @@
"release": "bumpp",
"test": "TZ=UTC vitest",
"typecheck": "vue-tsc --noEmit",
"codegen": "graphql-codegen",
"docs:dev": "vitepress dev docs --open",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/language": "^6.12.1",
"@codemirror/state": "^6.5.3",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.9",
"@duckdb/duckdb-wasm": "1.33.1-dev18.0",
"@iconify-json/carbon": "^1.2.18",
"@iconify-json/cil": "^1.2.3",
@@ -39,8 +46,10 @@
"@iconify-json/octicon": "^1.2.20",
"@iconify-json/ph": "^1.2.2",
"@intlify/unplugin-vue-i18n": "^11.0.3",
"@lezer/highlight": "^1.2.3",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "4.1.18",
"@urql/vue": "^2.0.0",
"@vueuse/components": "^14.1.0",
"@vueuse/core": "^14.1.0",
"@vueuse/integrations": "^14.1.0",
@@ -49,6 +58,7 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"ansi-to-html": "^0.7.2",
"codemirror": "^6.0.2",
"daisyui": "5.5.14",
"entities": "^7.0.0",
"fuse.js": "^7.1.0",
@@ -63,6 +73,7 @@
"unplugin-vue-components": "^31.0.0",
"unplugin-vue-macros": "^2.14.5",
"unplugin-vue-router": "^0.19.2",
"urql": "^5.0.1",
"vite": "7.3.1",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-svg-loader": "^5.1.0",
@@ -73,6 +84,11 @@
},
"devDependencies": {
"@apache-arrow/esnext-esm": "^21.1.0",
"@graphql-codegen/cli": "^6.1.1",
"@graphql-codegen/typed-document-node": "^6.1.5",
"@graphql-codegen/typescript": "^5.0.7",
"@graphql-codegen/typescript-operations": "^5.0.7",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/ion": "^1.2.6",
"@iconify-json/material-symbols-light": "^1.2.52",
"@iconify-json/ri": "^1.2.7",
@@ -87,6 +103,7 @@
"c8": "^10.1.3",
"concurrently": "^9.2.1",
"eventsourcemock": "^2.0.0",
"graphql": "^16.12.0",
"jsdom": "^27.4.0",
"lint-staged": "^16.2.7",
"prettier": "^3.8.0",
+2623 -6
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -28,6 +28,7 @@ export default defineConfig(() => ({
polyfill: false,
},
target: "esnext",
chunkSizeWarningLimit: 600,
},
plugins: [
VueRouter({
@@ -35,7 +36,7 @@ export default defineConfig(() => ({
src: "./assets/pages",
},
dts: "./assets/typed-router.d.ts",
importMode: "sync",
importMode: "sync", // this can't be async because it won't work with custom base
}),
VueMacros({
plugins: {