mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Centralize alert webhook service metadata
This commit is contained in:
parent
062721f020
commit
3aea37c3d2
9 changed files with 198 additions and 101 deletions
|
|
@ -69,6 +69,10 @@ The alert webhook editor now mirrors that canonical Pushover field rule through
|
|||
same alias and preset mapping instead of carrying its own local webhook-field
|
||||
normalization fork.
|
||||
|
||||
The alert webhook service chooser also now derives its service set from the
|
||||
backend webhook template registry, rather than keeping a second frontend-only
|
||||
list of services and descriptions.
|
||||
|
||||
Frontend alert surfaces and backend alert-support files now require explicit
|
||||
registry path-policy coverage, so new alert-owned runtime files must be mapped
|
||||
to a concrete proof route instead of silently inheriting subsystem-default
|
||||
|
|
|
|||
|
|
@ -68,13 +68,19 @@ webhook delivery now also share one owned rendering path for URL rendering,
|
|||
service-specific enrichment, and template selection. When persistent queue
|
||||
bootstrap fails, the runtime now falls back to an in-memory queue owner
|
||||
instead of dropping into a separate nil-queue direct-send mode. Service-specific
|
||||
webhook compatibility like Pushover `app_token` / `user_token` legacy fields is
|
||||
webhook compatibility like Pushover `app_token` / `user_token` legacy fields is
|
||||
now canonicalized at webhook-config ownership boundaries, so runtime delivery
|
||||
only handles canonical `token` / `user` fields instead of injecting aliases
|
||||
mid-flight. That boundary includes config persistence plus API/UI ingress for
|
||||
create, update, and ad hoc test requests; `internal/notifications/` may not
|
||||
silently rewrite those legacy keys once webhook state is already live in the
|
||||
runtime.
|
||||
|
||||
The webhook template registry is also the canonical source of truth for the
|
||||
alert webhook service set and the metadata the frontend uses to build its
|
||||
service chooser. Frontend presentation may format the options, but it must
|
||||
derive the available services from the backend template registry instead of
|
||||
keeping a second hardcoded service list.
|
||||
Email single-alert, grouped, resolved, and HTML send paths must follow that
|
||||
same ownership rule: they may expose different calling surfaces, but they must
|
||||
all route through one canonical enhanced email executor instead of rebuilding
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ function makeWebhook(overrides: Partial<Webhook> = {}): Webhook {
|
|||
const discordTemplate = {
|
||||
service: 'discord',
|
||||
name: 'Discord Webhook',
|
||||
description: 'Discord server webhook',
|
||||
urlPattern: 'https://discord.com/api/webhooks/...',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -50,6 +51,7 @@ const discordTemplate = {
|
|||
const slackTemplate = {
|
||||
service: 'slack',
|
||||
name: 'Slack Webhook',
|
||||
description: 'Slack incoming webhook',
|
||||
urlPattern: 'https://hooks.slack.com/services/...',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -60,6 +62,7 @@ const slackTemplate = {
|
|||
const genericTemplate = {
|
||||
service: 'generic',
|
||||
name: 'Generic Webhook',
|
||||
description: 'Custom webhook endpoint',
|
||||
urlPattern: 'https://example.com/webhook',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -67,7 +70,18 @@ const genericTemplate = {
|
|||
instructions: '',
|
||||
};
|
||||
|
||||
const mockTemplates = [genericTemplate, discordTemplate, slackTemplate];
|
||||
const pushoverTemplate = {
|
||||
service: 'pushover',
|
||||
name: 'Pushover',
|
||||
description: 'Mobile push notifications',
|
||||
urlPattern: 'https://api.pushover.net/1/messages.json',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
payloadTemplate: '',
|
||||
instructions: 'Create an application at https://pushover.net/apps',
|
||||
};
|
||||
|
||||
const mockTemplates = [genericTemplate, discordTemplate, slackTemplate, pushoverTemplate];
|
||||
|
||||
// --- Tests ---
|
||||
describe('WebhookConfig', () => {
|
||||
|
|
@ -663,7 +677,7 @@ describe('WebhookConfig', () => {
|
|||
|
||||
// --- Service selection ---
|
||||
|
||||
it('toggles service dropdown when service type button is clicked', () => {
|
||||
it('toggles service dropdown when service type button is clicked', async () => {
|
||||
render(() => (
|
||||
<WebhookConfig
|
||||
webhooks={[]}
|
||||
|
|
@ -680,14 +694,14 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Service options should appear
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
expect(screen.getByText('Telegram')).toBeInTheDocument();
|
||||
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
|
||||
expect(screen.getByText('PagerDuty')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pushover')).toBeInTheDocument();
|
||||
expect(screen.getByText('Gotify')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies template settings when selecting a service', async () => {
|
||||
|
|
@ -713,6 +727,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select Discord
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
|
@ -753,6 +771,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
||||
|
|
@ -784,6 +806,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
||||
|
|
@ -851,6 +877,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
||||
|
|
@ -1160,6 +1190,9 @@ describe('WebhookConfig', () => {
|
|||
});
|
||||
|
||||
fireEvent.click(screen.getByText(/Generic →/));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Pushover')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Pushover').closest('button')!);
|
||||
|
||||
expect(screen.getByText('Application Token')).toBeInTheDocument();
|
||||
|
|
@ -1295,6 +1328,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
||||
|
|
@ -1432,6 +1469,10 @@ describe('WebhookConfig', () => {
|
|||
const serviceBtn = screen.getByText(/Generic →/);
|
||||
fireEvent.click(serviceBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const discordBtn = screen.getByText('Discord server webhook').closest('button')!;
|
||||
fireEvent.click(discordBtn);
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import {
|
|||
interface WebhookTemplate {
|
||||
service: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
urlPattern: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
|
|
@ -446,7 +447,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
|
||||
<Show when={showServiceDropdown()}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 border border-border px-3 py-2 mb-3 text-xs">
|
||||
<For each={getAlertWebhookServices()}>
|
||||
<For each={getAlertWebhookServices(templates())}>
|
||||
{(service) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||
import {
|
||||
ALERT_WEBHOOK_TEST_FAILURE,
|
||||
ALERT_WEBHOOK_TEST_SUCCESS,
|
||||
getAlertWebhookServices,
|
||||
getAlertWebhookCustomFieldPresets,
|
||||
getAlertWebhookTestFailure,
|
||||
getAlertWebhookTestSuccess,
|
||||
|
|
@ -54,4 +55,42 @@ describe('alertWebhookPresentation', () => {
|
|||
user_token: 'legacy-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('derives webhook service options from the backend template registry', () => {
|
||||
expect(
|
||||
getAlertWebhookServices([
|
||||
{
|
||||
service: 'discord',
|
||||
name: 'Discord Webhook',
|
||||
description: 'Discord server webhook',
|
||||
urlPattern: 'https://discord.com/api/webhooks/.../...',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
payloadTemplate: '',
|
||||
instructions: '',
|
||||
},
|
||||
{
|
||||
service: 'generic',
|
||||
name: 'Generic Webhook',
|
||||
description: 'Custom webhook endpoint',
|
||||
urlPattern: '',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
payloadTemplate: '',
|
||||
instructions: '',
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'discord',
|
||||
label: 'Discord',
|
||||
description: 'Discord server webhook',
|
||||
},
|
||||
{
|
||||
id: 'generic',
|
||||
label: 'Generic',
|
||||
description: 'Custom webhook endpoint',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2975,7 +2975,7 @@ describe('frontend resource type boundaries', () => {
|
|||
});
|
||||
|
||||
it('keeps alert webhook service vocabulary and action copy in a shared presentation utility', () => {
|
||||
expect(webhookConfigSource).toContain('getAlertWebhookServices');
|
||||
expect(webhookConfigSource).toContain('getAlertWebhookServices(templates())');
|
||||
expect(webhookConfigSource).toContain('getAlertWebhookServiceLabel');
|
||||
expect(webhookConfigSource).toContain('getAlertWebhookCustomFieldPresets');
|
||||
expect(webhookConfigSource).toContain('normalizeAlertWebhookCustomFields');
|
||||
|
|
@ -3000,13 +3000,13 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(webhookConfigSource).not.toContain('Enable All');
|
||||
expect(webhookConfigSource).not.toContain('Disable All');
|
||||
expect(webhookConfigSource).not.toContain('Enable this webhook');
|
||||
expect(alertWebhookPresentationSource).toContain('export const ALERT_WEBHOOK_SERVICES');
|
||||
expect(alertWebhookPresentationSource).toContain(
|
||||
'export function getAlertWebhookServiceLabel',
|
||||
);
|
||||
expect(alertWebhookPresentationSource).toContain(
|
||||
'export function getAlertWebhookServices',
|
||||
);
|
||||
expect(alertWebhookPresentationSource).not.toContain('ALERT_WEBHOOK_SERVICES');
|
||||
expect(alertWebhookPresentationSource).toContain(
|
||||
'export function getAlertWebhookCustomFieldPresets',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
export const ALERT_WEBHOOK_SERVICES = [
|
||||
'generic',
|
||||
'discord',
|
||||
'slack',
|
||||
'mattermost',
|
||||
'telegram',
|
||||
'teams',
|
||||
'teams-adaptive',
|
||||
'pagerduty',
|
||||
'pushover',
|
||||
'gotify',
|
||||
'ntfy',
|
||||
] as const;
|
||||
import type { WebhookTemplate as AlertWebhookTemplate } from '@/api/notifications';
|
||||
|
||||
type AlertWebhookService = (typeof ALERT_WEBHOOK_SERVICES)[number];
|
||||
type AlertWebhookService =
|
||||
| 'generic'
|
||||
| 'discord'
|
||||
| 'slack'
|
||||
| 'mattermost'
|
||||
| 'telegram'
|
||||
| 'teams'
|
||||
| 'teams-adaptive'
|
||||
| 'pagerduty'
|
||||
| 'pushover'
|
||||
| 'gotify'
|
||||
| 'ntfy';
|
||||
|
||||
type AlertWebhookServicePresentation = {
|
||||
label: string;
|
||||
description: string;
|
||||
mentionPlaceholder?: string;
|
||||
mentionHelp?: string;
|
||||
};
|
||||
|
|
@ -34,57 +32,46 @@ const ALERT_WEBHOOK_SERVICE_PRESENTATION: Record<
|
|||
> = {
|
||||
generic: {
|
||||
label: 'Generic',
|
||||
description: 'Custom webhook endpoint',
|
||||
},
|
||||
discord: {
|
||||
label: 'Discord',
|
||||
description: 'Discord server webhook',
|
||||
mentionPlaceholder: '@everyone or <@USER_ID> or <@&ROLE_ID>',
|
||||
mentionHelp: 'Discord: Use @everyone, @here, <@USER_ID>, or <@&ROLE_ID>',
|
||||
},
|
||||
slack: {
|
||||
label: 'Slack',
|
||||
description: 'Slack incoming webhook',
|
||||
mentionPlaceholder: '@channel, @here, or <@USER_ID>',
|
||||
mentionHelp: 'Slack: Use @channel, @here, <@USER_ID>, or <!subteam^ID>',
|
||||
},
|
||||
mattermost: {
|
||||
label: 'Mattermost',
|
||||
description: 'Mattermost incoming webhook',
|
||||
mentionPlaceholder: '@channel, @all, or @username',
|
||||
mentionHelp: 'Mattermost: Use @channel, @all, or @username',
|
||||
},
|
||||
telegram: {
|
||||
label: 'Telegram',
|
||||
description: 'Telegram bot notifications',
|
||||
},
|
||||
teams: {
|
||||
label: 'Microsoft Teams',
|
||||
description: 'Microsoft Teams webhook',
|
||||
mentionPlaceholder: '@General or user email',
|
||||
mentionHelp: 'Teams: Use channel names like @General',
|
||||
},
|
||||
'teams-adaptive': {
|
||||
label: 'Teams (Adaptive)',
|
||||
description: 'Teams with Adaptive Cards',
|
||||
mentionPlaceholder: '@General or user email',
|
||||
mentionHelp: 'Teams: Use channel names like @General',
|
||||
},
|
||||
pagerduty: {
|
||||
label: 'PagerDuty',
|
||||
description: 'PagerDuty Events API v2',
|
||||
},
|
||||
pushover: {
|
||||
label: 'Pushover',
|
||||
description: 'Mobile push notifications',
|
||||
},
|
||||
gotify: {
|
||||
label: 'Gotify',
|
||||
description: 'Self-hosted push notifications',
|
||||
},
|
||||
ntfy: {
|
||||
label: 'ntfy',
|
||||
description: 'Push notifications via ntfy.sh',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -163,11 +150,11 @@ export const ALERT_WEBHOOK_DISABLED_LABEL = 'Disabled';
|
|||
export const ALERT_WEBHOOK_DISABLE_ALL_LABEL = 'Disable All';
|
||||
export const ALERT_WEBHOOK_ENABLE_ALL_LABEL = 'Enable All';
|
||||
|
||||
export function getAlertWebhookServices() {
|
||||
return ALERT_WEBHOOK_SERVICES.map((service) => ({
|
||||
id: service,
|
||||
label: ALERT_WEBHOOK_SERVICE_PRESENTATION[service].label,
|
||||
description: ALERT_WEBHOOK_SERVICE_PRESENTATION[service].description,
|
||||
export function getAlertWebhookServices(templates: AlertWebhookTemplate[] = []) {
|
||||
return templates.map((template) => ({
|
||||
id: template.service,
|
||||
label: getAlertWebhookServiceLabel(template.service),
|
||||
description: template.description || template.name,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -175,13 +162,6 @@ export function getAlertWebhookServiceLabel(service: string) {
|
|||
return ALERT_WEBHOOK_SERVICE_PRESENTATION[service as AlertWebhookService]?.label || service;
|
||||
}
|
||||
|
||||
export function getAlertWebhookServiceDescription(service: string) {
|
||||
return (
|
||||
ALERT_WEBHOOK_SERVICE_PRESENTATION[service as AlertWebhookService]?.description ||
|
||||
ALERT_WEBHOOK_SERVICE_PRESENTATION.pagerduty.description
|
||||
);
|
||||
}
|
||||
|
||||
export function getAlertWebhookCustomFieldPresets(service: string) {
|
||||
return ALERT_WEBHOOK_CUSTOM_FIELD_PRESETS[service.trim().toLowerCase()];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,11 @@ func TestGetWebhookTemplates(t *testing.T) {
|
|||
t.Errorf("Template %q has empty Name", tmpl.Service)
|
||||
}
|
||||
|
||||
// Description must be non-empty so the frontend can derive the service chooser
|
||||
if tmpl.Description == "" {
|
||||
t.Errorf("Template %q has empty Description", tmpl.Service)
|
||||
}
|
||||
|
||||
// Method must be valid HTTP method
|
||||
validMethods := map[string]bool{"GET": true, "POST": true, "PUT": true, "PATCH": true, "DELETE": true}
|
||||
if !validMethods[tmpl.Method] {
|
||||
|
|
@ -228,6 +233,15 @@ func TestGetWebhookTemplates_KnownServices(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetWebhookTemplates_Descriptions(t *testing.T) {
|
||||
templates := GetWebhookTemplates()
|
||||
for _, tmpl := range templates {
|
||||
if tmpl.Description == "" {
|
||||
t.Fatalf("template %q has empty description", tmpl.Service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWebhookTemplates_DiscordSettings(t *testing.T) {
|
||||
templates := GetWebhookTemplates()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package notifications
|
|||
type WebhookTemplate struct {
|
||||
Service string `json:"service"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
URLPattern string `json:"urlPattern"`
|
||||
Method string `json:"method"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
|
|
@ -16,11 +17,12 @@ type WebhookTemplate struct {
|
|||
func GetWebhookTemplates() []WebhookTemplate {
|
||||
return []WebhookTemplate{
|
||||
{
|
||||
Service: "discord",
|
||||
Name: "Discord Webhook",
|
||||
URLPattern: "https://discord.com/api/webhooks/{webhook_id}/{webhook_token}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "discord",
|
||||
Name: "Discord Webhook",
|
||||
Description: "Discord server webhook",
|
||||
URLPattern: "https://discord.com/api/webhooks/{webhook_id}/{webhook_token}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"username": "Pulse Monitoring",
|
||||
{{if .Mention}}"content": "{{.Mention}}",{{end}}
|
||||
|
|
@ -64,11 +66,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In Discord, go to Server Settings > Integrations > Webhooks\n2. Create a new webhook and copy the URL\n3. Paste the URL here (format: https://discord.com/api/webhooks/...)\n4. Optional: Add a mention in the Mention field (e.g., @everyone, <@USER_ID>, <@&ROLE_ID>)",
|
||||
},
|
||||
{
|
||||
Service: "telegram",
|
||||
Name: "Telegram Bot",
|
||||
URLPattern: "https://api.telegram.org/bot{bot_token}/sendMessage",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "telegram",
|
||||
Name: "Telegram Bot",
|
||||
Description: "Telegram bot notifications",
|
||||
URLPattern: "https://api.telegram.org/bot{bot_token}/sendMessage",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"chat_id": "{{.ChatID}}",
|
||||
"text": "*Pulse Alert: {{.Level | title}}*\n\n{{.Message}}\n\n*Details:*\n• Resource: {{.ResourceName}}\n• Node: {{.Node}}\n• Type: {{.Type | title}}\n• Value: {{if or (eq .Type "diskRead") (eq .Type "diskWrite")}}{{printf "%.1f" .Value}} MB/s{{else}}{{printf "%.1f" .Value}}%{{end}}\n• Threshold: {{if or (eq .Type "diskRead") (eq .Type "diskWrite")}}{{printf "%.0f" .Threshold}} MB/s{{else}}{{printf "%.0f" .Threshold}}%{{end}}\n• Duration: {{.Duration}}\n\n[View in Pulse]({{.Instance}})",
|
||||
|
|
@ -84,11 +87,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. Create a bot with @BotFather on Telegram\n2. Get your bot token\n3. Get your chat ID by messaging the bot and visiting: https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates\n4. URL format: https://api.telegram.org/bot<BOT_TOKEN>/sendMessage?chat_id=<CHAT_ID>\n5. IMPORTANT: You MUST include ?chat_id=YOUR_CHAT_ID in the URL",
|
||||
},
|
||||
{
|
||||
Service: "slack",
|
||||
Name: "Slack Incoming Webhook",
|
||||
URLPattern: "https://hooks.slack.com/services/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "slack",
|
||||
Name: "Slack Incoming Webhook",
|
||||
Description: "Slack incoming webhook",
|
||||
URLPattern: "https://hooks.slack.com/services/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"text": "{{if .Mention}}{{.Mention}} {{end}}Pulse Alert: {{.Level | title}} - {{.ResourceName}}",
|
||||
"blocks": [
|
||||
|
|
@ -178,11 +182,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In Slack, go to Apps > Incoming Webhooks\n2. Add to Slack and choose a channel\n3. Copy the webhook URL and paste it here (format: https://hooks.slack.com/services/...)\n4. Optional: Add a mention in the Mention field (e.g., @channel, @here, <@USER_ID>, <!subteam^ID>)",
|
||||
},
|
||||
{
|
||||
Service: "teams",
|
||||
Name: "Microsoft Teams",
|
||||
URLPattern: "https://{tenant}.webhook.office.com/webhookb2/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "teams",
|
||||
Name: "Microsoft Teams",
|
||||
Description: "Microsoft Teams webhook",
|
||||
URLPattern: "https://{tenant}.webhook.office.com/webhookb2/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
|
|
@ -233,10 +238,11 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In Teams channel, click ... > Connectors\n2. Configure Incoming Webhook\n3. Copy the URL and paste it here\n4. Optional: Add a mention in the Mention field (e.g., @General)\n\nNote: MessageCard format is supported until December 2025. For new implementations, consider using Adaptive Cards.",
|
||||
},
|
||||
{
|
||||
Service: "pagerduty",
|
||||
Name: "PagerDuty Events API v2",
|
||||
URLPattern: "https://events.pagerduty.com/v2/enqueue",
|
||||
Method: "POST",
|
||||
Service: "pagerduty",
|
||||
Name: "PagerDuty Events API v2",
|
||||
Description: "PagerDuty Events API v2",
|
||||
URLPattern: "https://events.pagerduty.com/v2/enqueue",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/vnd.pagerduty+json;version=2",
|
||||
|
|
@ -277,11 +283,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In PagerDuty, go to Configuration > Services\n2. Add an integration > Events API V2\n3. Copy the Integration Key\n4. Add the key as a custom field named 'routing_key'\n\nNote: PagerDuty recommends using Events API v2 for new integrations.",
|
||||
},
|
||||
{
|
||||
Service: "teams-adaptive",
|
||||
Name: "Microsoft Teams (Adaptive Card)",
|
||||
URLPattern: "https://{tenant}.webhook.office.com/webhookb2/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "teams-adaptive",
|
||||
Name: "Microsoft Teams (Adaptive Card)",
|
||||
Description: "Teams with Adaptive Cards",
|
||||
URLPattern: "https://{tenant}.webhook.office.com/webhookb2/{webhook_path}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"type": "message",
|
||||
"attachments": [{
|
||||
|
|
@ -365,11 +372,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In Teams channel, click ... > Connectors\n2. Configure Incoming Webhook\n3. Copy the URL and paste it here\n\nThis uses the modern Adaptive Card format recommended for new implementations.",
|
||||
},
|
||||
{
|
||||
Service: "pushover",
|
||||
Name: "Pushover",
|
||||
URLPattern: "https://api.pushover.net/1/messages.json",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "pushover",
|
||||
Name: "Pushover",
|
||||
Description: "Mobile push notifications",
|
||||
URLPattern: "https://api.pushover.net/1/messages.json",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"token": "{{.CustomFields.token}}",
|
||||
"user": "{{.CustomFields.user}}",
|
||||
|
|
@ -392,11 +400,12 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. Create an application at https://pushover.net/apps\n2. Copy your Application Token\n3. Get your User Key from your Pushover dashboard\n4. URL: https://api.pushover.net/1/messages.json\n5. Add custom fields:\n • token: YOUR_APP_TOKEN (required)\n • user: YOUR_USER_KEY (required)\n • sound: notification sound (optional, e.g., spacealarm, siren, tugboat)\n • priority: -2 to 2 (optional, overrides level-based default)\n • device: specific device name (optional, overrides ResourceName)",
|
||||
},
|
||||
{
|
||||
Service: "gotify",
|
||||
Name: "Gotify",
|
||||
URLPattern: "https://{your-gotify-server}/message?token={your-app-token}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "gotify",
|
||||
Name: "Gotify",
|
||||
Description: "Self-hosted push notifications",
|
||||
URLPattern: "https://{your-gotify-server}/message?token={your-app-token}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"message": "**{{if eq .Level "critical"}}CRITICAL{{else if eq .Level "warning"}}WARNING{{else}}INFO{{end}}**: **{{.ResourceName}}** on **{{.Node}}**\n\n{{.Message}}\n\n**Alert Details:**\n- **Resource:** {{.ResourceName}}\n- **Node:** {{.Node}}\n- **Type:** {{.Type | title}}\n- **Current:** {{if or (eq .Type "diskRead") (eq .Type "diskWrite")}}{{printf "%.1f" .Value}} MB/s{{else}}{{printf "%.1f" .Value}}%{{end}}\n- **Threshold:** {{if or (eq .Type "diskRead") (eq .Type "diskWrite")}}{{printf "%.0f" .Threshold}} MB/s{{else}}{{printf "%.0f" .Threshold}}%{{end}}\n- **Duration:** {{.Duration}}\n- **Alert Identifier:** {{.ID}}\n\n[View in Pulse]({{.Instance}})",
|
||||
"title": "{{.ResourceName}} - {{.Type | title}} {{.Level | upper}} Alert",
|
||||
|
|
@ -439,10 +448,11 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
Instructions: "1. In Gotify, create a new application\n2. Copy the application token\n3. URL format: https://your-gotify-server/message?token=YOUR_APP_TOKEN\n4. The token must be included in the URL as a parameter",
|
||||
},
|
||||
{
|
||||
Service: "ntfy",
|
||||
Name: "ntfy.sh",
|
||||
URLPattern: "https://ntfy.sh/{topic}",
|
||||
Method: "POST",
|
||||
Service: "ntfy",
|
||||
Name: "ntfy.sh",
|
||||
Description: "Push notifications via ntfy.sh",
|
||||
URLPattern: "https://ntfy.sh/{topic}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "text/plain",
|
||||
// Note: Title, Priority, and Tags headers should be added dynamically based on alert level
|
||||
|
|
@ -465,11 +475,12 @@ View in Pulse: {{.Instance}}`,
|
|||
Instructions: "1. Choose a topic name (e.g., 'my-pulse-alerts')\n2. URL format: https://ntfy.sh/YOUR_TOPIC\n Or for self-hosted: https://your-ntfy-server/YOUR_TOPIC\n3. For authentication, add a custom header:\n • Header Name: Authorization\n • Header Value: Bearer YOUR_TOKEN (or Basic base64_encoded_credentials)\n4. Subscribe to the topic in your ntfy app using the same topic name",
|
||||
},
|
||||
{
|
||||
Service: "mattermost",
|
||||
Name: "Mattermost Incoming Webhook",
|
||||
URLPattern: "https://{your-mattermost-server}/hooks/{webhook_id}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "mattermost",
|
||||
Name: "Mattermost Incoming Webhook",
|
||||
Description: "Mattermost incoming webhook",
|
||||
URLPattern: "https://{your-mattermost-server}/hooks/{webhook_id}",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"username": "Pulse Monitoring",
|
||||
"icon_url": "https://raw.githubusercontent.com/rcourtman/Pulse/main/frontend-modern/public/android-chrome-192x192.png",
|
||||
|
|
@ -483,11 +494,12 @@ View in Pulse: {{.Instance}}`,
|
|||
Instructions: "1. In Mattermost, go to Integrations > Incoming Webhooks\n2. Create a new webhook and select the channel\n3. Copy the webhook URL and paste it here\n\nNote: This template uses Markdown formatting which is fully supported by Mattermost.",
|
||||
},
|
||||
{
|
||||
Service: "generic",
|
||||
Name: "Generic JSON Webhook",
|
||||
URLPattern: "",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
Service: "generic",
|
||||
Name: "Generic JSON Webhook",
|
||||
Description: "Custom webhook endpoint",
|
||||
URLPattern: "",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"alert": {
|
||||
"id": "{{.ID}}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue