diff --git a/assets/components/Notification/DestinationForm.vue b/assets/components/Notification/DestinationForm.vue
index 3e9617f3..f2b817eb 100644
--- a/assets/components/Notification/DestinationForm.vue
+++ b/assets/components/Notification/DestinationForm.vue
@@ -72,6 +72,38 @@
/>
+
+
+
+
+
+
{{ error }}
@@ -97,6 +129,55 @@
import { useMutation } from "@urql/vue";
import { CreateDispatcherDocument, UpdateDispatcherDocument, type Dispatcher } from "@/types/graphql";
+type PayloadFormat = "slack" | "discord" | "ntfy" | "custom";
+
+const PAYLOAD_TEMPLATES: Record
= {
+ slack: `{
+ "text": "{{ .Container.Name }}",
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "*{{ .Container.Name }}*\\n{{ .Log.Message }}"
+ }
+ },
+ {
+ "type": "context",
+ "elements": [
+ {
+ "type": "mrkdwn",
+ "text": "Host: {{ .Container.Host }} | Image: {{ .Container.Image }}"
+ }
+ ]
+ }
+ ]
+}`,
+ discord: `{
+ "content": "{{ .Container.Name }}",
+ "embeds": [
+ {
+ "title": "{{ .Container.Name }}",
+ "description": "{{ .Log.Message }}",
+ "fields": [
+ { "name": "Host", "value": "{{ .Container.Host }}", "inline": true },
+ { "name": "Image", "value": "{{ .Container.Image }}", "inline": true }
+ ]
+ }
+ ]
+}`,
+ ntfy: `{
+ "topic": "dozzle-{{ .Container.Host }}",
+ "title": "{{ .Container.Name }}",
+ "message": "{{ .Log.Message }}"
+}`,
+ custom: `{
+ "container": "{{ .Container.Name }}",
+ "level": "{{ .Log.Level }}",
+ "message": "{{ .Log.Message }}"
+}`,
+};
+
const { close, onCreated, destination } = defineProps<{
close?: () => void;
onCreated?: () => void;
@@ -106,17 +187,24 @@ const { close, onCreated, destination } = defineProps<{
const createMutation = useMutation(CreateDispatcherDocument);
const updateMutation = useMutation(UpdateDispatcherDocument);
-const isEditing = computed(() => !!destination);
+const isEditing = !!destination;
const nameInput = ref();
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 payloadFormat = ref(isEditing ? "custom" : "slack");
+const template = ref(isEditing ? (destination?.template ?? "") : PAYLOAD_TEMPLATES[payloadFormat.value]);
const isTesting = ref(false);
const isSaving = ref(false);
const error = ref(null);
+function selectPayloadFormat(format: PayloadFormat) {
+ payloadFormat.value = format;
+ template.value = PAYLOAD_TEMPLATES[format];
+}
+
const canTest = computed(() => {
if (type.value === "webhook") {
return webhookUrl.value.trim().length > 0;
@@ -158,9 +246,10 @@ async function saveDestination() {
name: name.value.trim(),
type: type.value,
url: type.value === "webhook" ? webhookUrl.value.trim() : undefined,
+ template: type.value === "webhook" && template.value.trim() ? template.value.trim() : undefined,
};
- const result = isEditing.value
+ const result = isEditing
? await updateMutation.executeMutation({ id: destination!.id, input })
: await createMutation.executeMutation({ input });
diff --git a/assets/graphql/notifications.graphql b/assets/graphql/notifications.graphql
index ba2f2a8e..2f1c44fc 100644
--- a/assets/graphql/notifications.graphql
+++ b/assets/graphql/notifications.graphql
@@ -23,6 +23,7 @@ query GetDispatchers {
name
type
url
+ template
}
}
@@ -90,6 +91,7 @@ mutation UpdateDispatcher($id: Int!, $input: DispatcherInput!) {
name
type
url
+ template
}
}
diff --git a/assets/types/graphql.ts b/assets/types/graphql.ts
index fbbb49f1..a8b33b99 100644
--- a/assets/types/graphql.ts
+++ b/assets/types/graphql.ts
@@ -38,12 +38,14 @@ export type Dispatcher = {
__typename?: 'Dispatcher';
id: Scalars['Int']['output'];
name: Scalars['String']['output'];
+ template?: Maybe;
type: Scalars['String']['output'];
url?: Maybe;
};
export type DispatcherInput = {
name: Scalars['String']['input'];
+ template?: InputMaybe;
type: Scalars['String']['input'];
url?: InputMaybe;
};
@@ -198,7 +200,7 @@ export type GetNotificationRulesQuery = { __typename?: 'Query', notificationRule
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 GetDispatchersQuery = { __typename?: 'Query', dispatchers: Array<{ __typename?: 'Dispatcher', id: number, name: string, type: string, url?: string | null, template?: string | null }> };
export type CreateNotificationRuleMutationVariables = Exact<{
input: NotificationRuleInput;
@@ -243,7 +245,7 @@ export type UpdateDispatcherMutationVariables = Exact<{
}>;
-export type UpdateDispatcherMutation = { __typename?: 'Mutation', updateDispatcher: { __typename?: 'Dispatcher', id: number, name: string, type: string, url?: string | null } };
+export type UpdateDispatcherMutation = { __typename?: 'Mutation', updateDispatcher: { __typename?: 'Dispatcher', id: number, name: string, type: string, url?: string | null, template?: string | null } };
export type DeleteDispatcherMutationVariables = Exact<{
id: Scalars['Int']['input'];
@@ -266,13 +268,13 @@ export type GetReleasesQuery = { __typename?: 'Query', releases: Array<{ __typen
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;
-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;
+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"}},{"kind":"Field","name":{"kind":"Name","value":"template"}}]}}]}}]} as unknown as DocumentNode;
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;
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;
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;
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;
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;
-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;
+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"}},{"kind":"Field","name":{"kind":"Name","value":"template"}}]}}]}}]} as unknown as DocumentNode;
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;
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;
export const GetReleasesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReleases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"releases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"mentionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"tag"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"htmlUrl"}},{"kind":"Field","name":{"kind":"Name","value":"latest"}},{"kind":"Field","name":{"kind":"Name","value":"features"}},{"kind":"Field","name":{"kind":"Name","value":"bugFixes"}},{"kind":"Field","name":{"kind":"Name","value":"breaking"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/graph/generated.go b/graph/generated.go
index 3dd73f56..251474b1 100644
--- a/graph/generated.go
+++ b/graph/generated.go
@@ -68,10 +68,11 @@ type ComplexityRoot struct {
}
Dispatcher struct {
- ID func(childComplexity int) int
- Name func(childComplexity int) int
- Type func(childComplexity int) int
- URL func(childComplexity int) int
+ ID func(childComplexity int) int
+ Name func(childComplexity int) int
+ Template func(childComplexity int) int
+ Type func(childComplexity int) int
+ URL func(childComplexity int) int
}
LogEvent struct {
@@ -267,6 +268,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Dispatcher.Name(childComplexity), true
+ case "Dispatcher.template":
+ if e.complexity.Dispatcher.Template == nil {
+ break
+ }
+
+ return e.complexity.Dispatcher.Template(childComplexity), true
case "Dispatcher.type":
if e.complexity.Dispatcher.Type == nil {
break
@@ -1351,6 +1358,35 @@ func (ec *executionContext) fieldContext_Dispatcher_url(_ context.Context, field
return fc, nil
}
+func (ec *executionContext) _Dispatcher_template(ctx context.Context, field graphql.CollectedField, obj *model.Dispatcher) (ret graphql.Marshaler) {
+ return graphql.ResolveField(
+ ctx,
+ ec.OperationContext,
+ field,
+ ec.fieldContext_Dispatcher_template,
+ func(ctx context.Context) (any, error) {
+ return obj.Template, nil
+ },
+ nil,
+ ec.marshalOString2áš–string,
+ true,
+ false,
+ )
+}
+
+func (ec *executionContext) fieldContext_Dispatcher_template(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Dispatcher",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _LogEvent_id(ctx context.Context, field graphql.CollectedField, obj *container.LogEvent) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -1811,6 +1847,8 @@ func (ec *executionContext) fieldContext_Mutation_createDispatcher(ctx context.C
return ec.fieldContext_Dispatcher_type(ctx, field)
case "url":
return ec.fieldContext_Dispatcher_url(ctx, field)
+ case "template":
+ return ec.fieldContext_Dispatcher_template(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
},
@@ -1862,6 +1900,8 @@ func (ec *executionContext) fieldContext_Mutation_updateDispatcher(ctx context.C
return ec.fieldContext_Dispatcher_type(ctx, field)
case "url":
return ec.fieldContext_Dispatcher_url(ctx, field)
+ case "template":
+ return ec.fieldContext_Dispatcher_template(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
},
@@ -2093,6 +2133,8 @@ func (ec *executionContext) fieldContext_NotificationRule_dispatcher(_ context.C
return ec.fieldContext_Dispatcher_type(ctx, field)
case "url":
return ec.fieldContext_Dispatcher_url(ctx, field)
+ case "template":
+ return ec.fieldContext_Dispatcher_template(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
},
@@ -2572,6 +2614,8 @@ func (ec *executionContext) fieldContext_Query_dispatchers(_ context.Context, fi
return ec.fieldContext_Dispatcher_type(ctx, field)
case "url":
return ec.fieldContext_Dispatcher_url(ctx, field)
+ case "template":
+ return ec.fieldContext_Dispatcher_template(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
},
@@ -2612,6 +2656,8 @@ func (ec *executionContext) fieldContext_Query_dispatcher(ctx context.Context, f
return ec.fieldContext_Dispatcher_type(ctx, field)
case "url":
return ec.fieldContext_Dispatcher_url(ctx, field)
+ case "template":
+ return ec.fieldContext_Dispatcher_template(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
},
@@ -4532,7 +4578,7 @@ func (ec *executionContext) unmarshalInputDispatcherInput(ctx context.Context, o
asMap[k] = v
}
- fieldsInOrder := [...]string{"name", "type", "url"}
+ fieldsInOrder := [...]string{"name", "type", "url", "template"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -4560,6 +4606,13 @@ func (ec *executionContext) unmarshalInputDispatcherInput(ctx context.Context, o
return it, err
}
it.URL = data
+ case "template":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("template"))
+ data, err := ec.unmarshalOString2áš–string(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.Template = data
}
}
@@ -4854,6 +4907,8 @@ func (ec *executionContext) _Dispatcher(ctx context.Context, sel ast.SelectionSe
}
case "url":
out.Values[i] = ec._Dispatcher_url(ctx, field, obj)
+ case "template":
+ out.Values[i] = ec._Dispatcher_template(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
diff --git a/graph/helpers.go b/graph/helpers.go
index 52295fa3..9b2cae85 100644
--- a/graph/helpers.go
+++ b/graph/helpers.go
@@ -40,11 +40,16 @@ func dispatcherConfigToDispatcher(d *notification.DispatcherConfig) *model.Dispa
if d.URL != "" {
url = &d.URL
}
+ var template *string
+ if d.Template != "" {
+ template = &d.Template
+ }
return &model.Dispatcher{
- ID: int32(d.ID),
- Name: d.Name,
- Type: d.Type,
- URL: url,
+ ID: int32(d.ID),
+ Name: d.Name,
+ Type: d.Type,
+ URL: url,
+ Template: template,
}
}
diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go
index d852b4fd..0bc692d5 100644
--- a/graph/model/models_gen.go
+++ b/graph/model/models_gen.go
@@ -9,16 +9,18 @@ import (
)
type Dispatcher struct {
- ID int32 `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- URL *string `json:"url,omitempty"`
+ ID int32 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ URL *string `json:"url,omitempty"`
+ Template *string `json:"template,omitempty"`
}
type DispatcherInput struct {
- Name string `json:"name"`
- Type string `json:"type"`
- URL *string `json:"url,omitempty"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ URL *string `json:"url,omitempty"`
+ Template *string `json:"template,omitempty"`
}
type Mutation struct {
diff --git a/graph/schema.graphqls b/graph/schema.graphqls
index f9f72940..204b2e87 100644
--- a/graph/schema.graphqls
+++ b/graph/schema.graphqls
@@ -46,6 +46,7 @@ type Dispatcher {
name: String!
type: String!
url: String
+ template: String
}
type PreviewResult {
@@ -90,6 +91,7 @@ input DispatcherInput {
name: String!
type: String!
url: String
+ template: String
}
input PreviewInput {
diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go
index 4eef77e8..33785bc7 100644
--- a/graph/schema.resolvers.go
+++ b/graph/schema.resolvers.go
@@ -122,7 +122,15 @@ func (r *mutationResolver) CreateDispatcher(ctx context.Context, input model.Dis
if input.URL != nil {
url = *input.URL
}
- d = dispatcher.NewWebhookDispatcher(input.Name, url)
+ templateStr := ""
+ if input.Template != nil {
+ templateStr = *input.Template
+ }
+ webhook, err := dispatcher.NewWebhookDispatcher(input.Name, url, templateStr)
+ if err != nil {
+ return nil, &Error{Message: err.Error()}
+ }
+ d = webhook
default:
return nil, &Error{Message: "unknown dispatcher type"}
}
@@ -130,10 +138,11 @@ func (r *mutationResolver) CreateDispatcher(ctx context.Context, input model.Dis
id := r.HostService.AddDispatcher(d)
return &model.Dispatcher{
- ID: int32(id),
- Name: input.Name,
- Type: input.Type,
- URL: input.URL,
+ ID: int32(id),
+ Name: input.Name,
+ Type: input.Type,
+ URL: input.URL,
+ Template: input.Template,
}, nil
}
@@ -146,7 +155,15 @@ func (r *mutationResolver) UpdateDispatcher(ctx context.Context, id int32, input
if input.URL != nil {
url = *input.URL
}
- d = dispatcher.NewWebhookDispatcher(input.Name, url)
+ templateStr := ""
+ if input.Template != nil {
+ templateStr = *input.Template
+ }
+ webhook, err := dispatcher.NewWebhookDispatcher(input.Name, url, templateStr)
+ if err != nil {
+ return nil, &Error{Message: err.Error()}
+ }
+ d = webhook
default:
return nil, &Error{Message: "unknown dispatcher type"}
}
@@ -154,10 +171,11 @@ func (r *mutationResolver) UpdateDispatcher(ctx context.Context, id int32, input
r.HostService.UpdateDispatcher(int(id), d)
return &model.Dispatcher{
- ID: id,
- Name: input.Name,
- Type: input.Type,
- URL: input.URL,
+ ID: id,
+ Name: input.Name,
+ Type: input.Type,
+ URL: input.URL,
+ Template: input.Template,
}, nil
}
diff --git a/internal/notification/dispatcher/webhook.go b/internal/notification/dispatcher/webhook.go
index 5e1fe6d4..d7c40803 100644
--- a/internal/notification/dispatcher/webhook.go
+++ b/internal/notification/dispatcher/webhook.go
@@ -5,33 +5,62 @@ import (
"context"
"encoding/json"
"fmt"
+ "io"
"net/http"
+ "text/template"
"time"
+
+ "github.com/rs/zerolog/log"
)
// WebhookDispatcher sends notifications to a webhook URL
type WebhookDispatcher struct {
- Name string
- URL string
- client *http.Client
+ Name string
+ URL string
+ Template *template.Template
+ TemplateText string // Original template string for serialization
+ client *http.Client
}
// NewWebhookDispatcher creates a new webhook dispatcher
-func NewWebhookDispatcher(name, url string) *WebhookDispatcher {
- return &WebhookDispatcher{
- Name: name,
- URL: url,
+// If templateStr is empty, the notification will be marshaled as JSON directly
+func NewWebhookDispatcher(name, url, templateStr string) (*WebhookDispatcher, error) {
+ w := &WebhookDispatcher{
+ Name: name,
+ URL: url,
+ TemplateText: templateStr,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
+
+ if templateStr != "" {
+ tmpl, err := template.New("webhook").Parse(templateStr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse template: %w", err)
+ }
+ w.Template = tmpl
+ }
+
+ return w, nil
}
// 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)
+ var payload []byte
+ var err error
+
+ if w.Template != nil {
+ var buf bytes.Buffer
+ if err := w.Template.Execute(&buf, notification); err != nil {
+ return fmt.Errorf("failed to execute template: %w", err)
+ }
+ payload = buf.Bytes()
+ } else {
+ 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))
@@ -48,6 +77,14 @@ func (w *WebhookDispatcher) Send(ctx context.Context, notification any) error {
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ responseBody, _ := io.ReadAll(resp.Body)
+ log.Debug().
+ Str("webhook", w.Name).
+ Str("url", w.URL).
+ Int("status_code", resp.StatusCode).
+ Str("payload", string(payload)).
+ Str("response_body", string(responseBody)).
+ Msg("webhook returned non-success status code")
return fmt.Errorf("webhook returned non-success status code: %d", resp.StatusCode)
}
diff --git a/internal/notification/log_listener.go b/internal/notification/log_listener.go
index cd56801a..d4adcaea 100644
--- a/internal/notification/log_listener.go
+++ b/internal/notification/log_listener.go
@@ -131,15 +131,9 @@ func (l *ContainerLogListener) startListening(c container.Container, client cont
// 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
- }
+ if found, err := client.FindContainer(l.ctx, c.ID, nil); err == nil {
+ l.startListening(found, client)
+ return
}
}
log.Warn().Str("containerID", c.ID).Msg("Could not find client for container")
@@ -150,7 +144,7 @@ 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")
+ log.Debug().Str("containerID", containerID).Msg("Stopped listening to container")
}
}
diff --git a/internal/notification/manager.go b/internal/notification/manager.go
index 341e90d9..c440eb4b 100644
--- a/internal/notification/manager.go
+++ b/internal/notification/manager.go
@@ -92,7 +92,7 @@ func (m *Manager) AddSubscription(sub *Subscription) error {
}
m.subscriptions.Store(sub.ID, sub)
- log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Added subscription")
+ log.Debug().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 {
@@ -107,7 +107,7 @@ func (m *Manager) AddSubscription(sub *Subscription) error {
// 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")
+ log.Debug().Int("id", id).Str("name", sub.Name).Msg("Removed subscription")
// Update listener to stop streams that are no longer needed
if m.listener != nil {
@@ -146,7 +146,7 @@ func (m *Manager) ReplaceSubscription(sub *Subscription) error {
}
m.subscriptions.Store(sub.ID, sub)
- log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Replaced subscription")
+ log.Debug().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 {
@@ -246,20 +246,20 @@ func (m *Manager) UpdateSubscription(id int, updates map[string]any) error {
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")
+ log.Debug().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")
+ log.Debug().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")
+ log.Debug().Int("id", id).Msg("Removed dispatcher")
}
}
@@ -283,10 +283,11 @@ func (m *Manager) Dispatchers() []DispatcherConfig {
switch v := d.(type) {
case *dispatcher.WebhookDispatcher:
result = append(result, DispatcherConfig{
- ID: id,
- Name: v.Name,
- Type: "webhook",
- URL: v.URL,
+ ID: id,
+ Name: v.Name,
+ Type: "webhook",
+ URL: v.URL,
+ Template: v.TemplateText,
})
}
return true
@@ -427,12 +428,16 @@ func (m *Manager) LoadConfig(r io.Reader) error {
var d dispatcher.Dispatcher
switch dispatcherConfig.Type {
case "webhook":
- d = dispatcher.NewWebhookDispatcher(dispatcherConfig.Name, dispatcherConfig.URL)
+ webhook, err := dispatcher.NewWebhookDispatcher(dispatcherConfig.Name, dispatcherConfig.URL, dispatcherConfig.Template)
+ if err != nil {
+ return fmt.Errorf("failed to create webhook dispatcher %s: %w", dispatcherConfig.Name, err)
+ }
+ d = webhook
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")
+ log.Debug().Int("id", dispatcherConfig.ID).Msg("Loaded dispatcher")
}
// Update listener to start streams for loaded subscriptions
@@ -466,6 +471,6 @@ func (m *Manager) loadSubscription(sub *Subscription) error {
}
m.subscriptions.Store(sub.ID, sub)
- log.Info().Str("name", sub.Name).Int("id", sub.ID).Msg("Loaded subscription")
+ log.Debug().Str("name", sub.Name).Int("id", sub.ID).Msg("Loaded subscription")
return nil
}
diff --git a/internal/notification/types.go b/internal/notification/types.go
index d947a4a2..696d7bbf 100644
--- a/internal/notification/types.go
+++ b/internal/notification/types.go
@@ -124,10 +124,11 @@ func (s *Subscription) AddTriggeredContainer(id string) {
// 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"`
+ 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"`
+ Template string `json:"template,omitempty" yaml:"template,omitempty"` // Go template for custom payload format
}
// Config represents the persisted notification configuration
diff --git a/locales/en.yml b/locales/en.yml
index d5e14c31..28e2f103 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -224,6 +224,13 @@ notifications:
cloud-description: Push, email, and dashboard
webhook-url: Webhook URL
webhook-url-placeholder: https://hooks.foo.com/services/...
+ payload-format: Payload Format
+ format-slack: Slack
+ format-discord: Discord
+ format-ntfy: ntfy
+ format-custom: Custom
+ template: Template
+ template-hint: Go template syntax
test: Test
cancel: Cancel
save: Save