mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-09 19:32:24 +00:00
feat: add custom webhook payload templates and fix Telegram chat_id handling
- Add custom JSON payload template support for generic webhooks - Users can now define custom webhook formats with Go template syntax - Fix Telegram webhook issue where chat_id in URL caused 400 errors - Automatically strip chat_id from URL and place in JSON body for Telegram - Add comprehensive webhook documentation with examples - Update API documentation with webhook endpoints Addresses #305
This commit is contained in:
parent
6765db9b13
commit
b77df2f2f1
10 changed files with 542 additions and 81 deletions
11
README.md
11
README.md
|
|
@ -243,6 +243,17 @@ docker logs pulse
|
|||
journalctl -u pulse -f
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Configuration Guide](docs/CONFIGURATION.md) - Complete setup and configuration
|
||||
- [API Reference](docs/API.md) - REST API endpoints and examples
|
||||
- [Webhook Guide](docs/WEBHOOKS.md) - Setting up webhooks and custom payloads
|
||||
- [Reverse Proxy Setup](docs/REVERSE_PROXY.md) - nginx, Caddy, Apache, Traefik configs
|
||||
- [PBS Agent](docs/PBS-AGENT.md) - Monitoring isolated PBS servers
|
||||
- [Security](docs/SECURITY.md) - Security features and best practices
|
||||
- [FAQ](docs/FAQ.md) - Common questions and troubleshooting
|
||||
- [Migration Guide](docs/MIGRATION_V3_TO_V4.md) - Upgrading from v3 to v4
|
||||
|
||||
## Security
|
||||
|
||||
- Credentials stored encrypted (AES-256-GCM)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# Pulse v4.3.0 Release Notes
|
||||
|
||||
## ⚠️ BREAKING CHANGES
|
||||
|
||||
### Frontend Now Embedded in Binary
|
||||
Starting with v4.3.0, the frontend is embedded directly in the Pulse binary. This is a significant architectural change that simplifies deployment but has some implications:
|
||||
|
||||
**What Changed:**
|
||||
- Frontend files are now compiled into the Go binary at build time
|
||||
- No separate `frontend-modern/` directory needed
|
||||
- Single binary deployment - truly portable
|
||||
- Smaller release tarballs
|
||||
|
||||
**Migration Notes:**
|
||||
- **For manual installations**: Simply replace your binary. The old `frontend-modern/` directory can be deleted
|
||||
- **For Docker users**: No changes needed, containers will work as normal
|
||||
- **For developers**: Frontend changes now require recompiling the Go binary (use `scripts/rebuild-frontend.sh`)
|
||||
- **Auto-updates**: Will work seamlessly from v4.2.1 to v4.3.0+
|
||||
|
||||
**Benefits:**
|
||||
- Eliminates redirect loop issues (#304)
|
||||
- No more path resolution problems
|
||||
- Simpler installation and deployment
|
||||
- Consistent behavior across all installation methods
|
||||
- Reduced deployment size
|
||||
|
||||
## Features
|
||||
- Embedded frontend for simplified single-binary deployment
|
||||
- Improved installation experience
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed redirect loops in various installation scenarios (#304)
|
||||
- Fixed path resolution issues with community scripts vs manual installs
|
||||
|
||||
## Technical Details
|
||||
The frontend is embedded using Go's `embed` package. During the build process:
|
||||
1. Frontend is built with npm
|
||||
2. Built files are copied to `internal/api/frontend-modern/`
|
||||
3. Go compiler embeds these files into the binary
|
||||
4. Runtime serves embedded files directly from memory
|
||||
|
||||
This change makes Pulse a true single-binary application, eliminating an entire class of deployment issues.
|
||||
53
docs/API.md
53
docs/API.md
|
|
@ -177,6 +177,59 @@ POST /api/notifications/email/test # Send test email
|
|||
GET /api/notifications/email-providers # List email providers
|
||||
```
|
||||
|
||||
### Webhook Configuration
|
||||
Manage webhook notification endpoints.
|
||||
|
||||
```bash
|
||||
GET /api/notifications/webhooks # List all webhooks
|
||||
POST /api/notifications/webhooks # Create new webhook
|
||||
PUT /api/notifications/webhooks/<id> # Update webhook
|
||||
DELETE /api/notifications/webhooks/<id> # Delete webhook
|
||||
POST /api/notifications/webhooks/test # Test webhook
|
||||
GET /api/notifications/webhook-templates # Get service templates
|
||||
```
|
||||
|
||||
#### Create Webhook Example
|
||||
```bash
|
||||
curl -X POST http://localhost:7655/api/notifications/webhooks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: your-token" \
|
||||
-d '{
|
||||
"name": "Discord Alert",
|
||||
"url": "https://discord.com/api/webhooks/xxx/yyy",
|
||||
"method": "POST",
|
||||
"service": "discord",
|
||||
"enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
#### Custom Payload Template Example
|
||||
```bash
|
||||
curl -X POST http://localhost:7655/api/notifications/webhooks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: your-token" \
|
||||
-d '{
|
||||
"name": "Custom Webhook",
|
||||
"url": "https://my-service.com/webhook",
|
||||
"method": "POST",
|
||||
"service": "generic",
|
||||
"enabled": true,
|
||||
"template": "{\"alert\": \"{{.Level}}: {{.Message}}\", \"value\": {{.Value}}}"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Test Webhook
|
||||
```bash
|
||||
curl -X POST http://localhost:7655/api/notifications/webhooks/test \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: your-token" \
|
||||
-d '{
|
||||
"name": "Test",
|
||||
"url": "https://example.com/webhook",
|
||||
"service": "generic"
|
||||
}'
|
||||
```
|
||||
|
||||
### Alert Thresholds
|
||||
Manage alert thresholds and overrides.
|
||||
|
||||
|
|
|
|||
262
docs/WEBHOOKS.md
Normal file
262
docs/WEBHOOKS.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Webhook Configuration Guide
|
||||
|
||||
Pulse supports sending alert notifications to various webhook services including Discord, Slack, Microsoft Teams, Telegram, PagerDuty, and any custom webhook endpoint.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Navigate to **Settings** → **Alerts** → **Webhooks**
|
||||
2. Click **Add Webhook**
|
||||
3. Select your service type or choose "Generic" for custom webhooks
|
||||
4. Enter the webhook URL and configure settings
|
||||
5. Test the webhook to ensure it's working
|
||||
6. Save your configuration
|
||||
|
||||
## Supported Services
|
||||
|
||||
### Discord
|
||||
```
|
||||
URL Format: https://discord.com/api/webhooks/{webhook_id}/{webhook_token}
|
||||
```
|
||||
1. In Discord, go to Server Settings → Integrations → Webhooks
|
||||
2. Create a new webhook and copy the URL
|
||||
3. Paste the URL in Pulse
|
||||
|
||||
### Telegram
|
||||
```
|
||||
URL Format: https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}
|
||||
```
|
||||
1. Create a bot with @BotFather on Telegram
|
||||
2. Get your bot token
|
||||
3. Get your chat ID by messaging the bot and visiting: `https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
|
||||
4. Use the URL format above (Pulse will handle chat_id correctly)
|
||||
|
||||
### Slack
|
||||
```
|
||||
URL Format: https://hooks.slack.com/services/{webhook_path}
|
||||
```
|
||||
1. In Slack, go to Apps → Incoming Webhooks
|
||||
2. Add to Slack and choose a channel
|
||||
3. Copy the webhook URL
|
||||
|
||||
### Microsoft Teams
|
||||
```
|
||||
URL Format: https://{tenant}.webhook.office.com/webhookb2/{webhook_path}
|
||||
```
|
||||
1. In Teams channel, click ... → Connectors
|
||||
2. Configure Incoming Webhook
|
||||
3. Copy the URL
|
||||
|
||||
### PagerDuty
|
||||
```
|
||||
URL: https://events.pagerduty.com/v2/enqueue
|
||||
```
|
||||
1. In PagerDuty, go to Configuration → Services
|
||||
2. Add an integration → Events API V2
|
||||
3. Copy the Integration Key
|
||||
4. Add the key as a header: `routing_key: YOUR_KEY`
|
||||
|
||||
## Custom Payload Templates
|
||||
|
||||
For generic webhooks, you can define custom JSON payloads using Go template syntax.
|
||||
|
||||
### Available Variables
|
||||
|
||||
| Variable | Description | Example Value |
|
||||
|----------|-------------|---------------|
|
||||
| `{{.ID}}` | Alert ID | "alert-123" |
|
||||
| `{{.Level}}` | Alert level | "warning", "critical" |
|
||||
| `{{.Type}}` | Resource type | "cpu", "memory", "disk" |
|
||||
| `{{.ResourceName}}` | Name of the resource | "Web Server VM" |
|
||||
| `{{.ResourceID}}` | Resource identifier | "vm-100" |
|
||||
| `{{.Node}}` | Proxmox node name | "pve-node-01" |
|
||||
| `{{.Instance}}` | Proxmox instance URL | "https://192.168.1.100:8006" |
|
||||
| `{{.Message}}` | Alert message | "CPU usage exceeded 90%" |
|
||||
| `{{.Value}}` | Current metric value | 95.5 |
|
||||
| `{{.Threshold}}` | Alert threshold | 90.0 |
|
||||
| `{{.Duration}}` | How long alert has been active | "5m" |
|
||||
| `{{.Timestamp}}` | Current timestamp | "2024-01-15T10:30:00Z" |
|
||||
| `{{.StartTime}}` | When alert started | "2024-01-15T10:25:00Z" |
|
||||
|
||||
### Template Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{.Level \| title}}` | Capitalize first letter | "Warning" |
|
||||
| `{{.Level \| upper}}` | Uppercase | "WARNING" |
|
||||
| `{{.Level \| lower}}` | Lowercase | "warning" |
|
||||
| `{{printf "%.1f" .Value}}` | Format numbers | "95.5" |
|
||||
|
||||
### Example Templates
|
||||
|
||||
#### Simple JSON
|
||||
```json
|
||||
{
|
||||
"text": "Alert: {{.Level}} - {{.Message}}",
|
||||
"resource": "{{.ResourceName}}",
|
||||
"value": {{.Value}},
|
||||
"threshold": {{.Threshold}}
|
||||
}
|
||||
```
|
||||
|
||||
#### Formatted Alert
|
||||
```json
|
||||
{
|
||||
"alert": {
|
||||
"level": "{{.Level | upper}}",
|
||||
"message": "{{.Message}}",
|
||||
"details": {
|
||||
"resource": "{{.ResourceName}}",
|
||||
"node": "{{.Node}}",
|
||||
"current_value": "{{printf "%.1f" .Value}}%",
|
||||
"threshold": "{{printf "%.0f" .Threshold}}%",
|
||||
"duration": "{{.Duration}}"
|
||||
}
|
||||
},
|
||||
"timestamp": "{{.Timestamp}}"
|
||||
}
|
||||
```
|
||||
|
||||
#### Slack-Compatible Custom Format
|
||||
```json
|
||||
{
|
||||
"text": "Pulse Alert",
|
||||
"attachments": [{
|
||||
"color": "{{if eq .Level "critical"}}danger{{else}}warning{{end}}",
|
||||
"title": "{{.Level | title}} Alert: {{.ResourceName}}",
|
||||
"text": "{{.Message}}",
|
||||
"fields": [
|
||||
{"title": "Value", "value": "{{printf "%.1f" .Value}}%", "short": true},
|
||||
{"title": "Threshold", "value": "{{printf "%.0f" .Threshold}}%", "short": true},
|
||||
{"title": "Node", "value": "{{.Node}}", "short": true},
|
||||
{"title": "Duration", "value": "{{.Duration}}", "short": true}
|
||||
],
|
||||
"footer": "Pulse Monitoring",
|
||||
"ts": {{.Timestamp}}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### Home Assistant
|
||||
```json
|
||||
{
|
||||
"title": "Pulse Alert: {{.Level | title}}",
|
||||
"message": "{{.Message}}",
|
||||
"data": {
|
||||
"entity_id": "sensor.{{.Node | lower}}_{{.Type}}",
|
||||
"state": {{.Value}},
|
||||
"attributes": {
|
||||
"resource": "{{.ResourceName}}",
|
||||
"threshold": {{.Threshold}},
|
||||
"duration": "{{.Duration}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### n8n / Node-RED
|
||||
```json
|
||||
{
|
||||
"workflow": "pulse_alert",
|
||||
"data": {
|
||||
"alert_id": "{{.ID}}",
|
||||
"level": "{{.Level}}",
|
||||
"resource": "{{.ResourceName}}",
|
||||
"node": "{{.Node}}",
|
||||
"metric": {
|
||||
"type": "{{.Type}}",
|
||||
"value": {{.Value}},
|
||||
"threshold": {{.Threshold}}
|
||||
},
|
||||
"message": "{{.Message}}",
|
||||
"timestamp": "{{.Timestamp}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Webhooks
|
||||
|
||||
1. After configuring a webhook, click the **Test** button
|
||||
2. Pulse will send a test alert to verify the webhook is working
|
||||
3. Check the receiving service to confirm the message arrived
|
||||
4. If the test fails, verify:
|
||||
- The URL is correct and accessible
|
||||
- Any required authentication tokens are included
|
||||
- The payload format matches what the service expects
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Webhook Returns 400 Bad Request
|
||||
- Check if the payload format is correct for your service
|
||||
- For Telegram, ensure chat_id is in the URL (Pulse handles it automatically)
|
||||
- Verify all required fields are present in custom templates
|
||||
|
||||
### Webhook Returns 401/403
|
||||
- Check authentication tokens/keys
|
||||
- Verify the webhook URL hasn't expired
|
||||
- Ensure IP restrictions allow Pulse server
|
||||
|
||||
### No Notifications Received
|
||||
- Verify the webhook is enabled
|
||||
- Check alert thresholds are configured correctly
|
||||
- Ensure notification cooldown period has passed
|
||||
- Test the webhook manually using the Test button
|
||||
|
||||
## API Reference
|
||||
|
||||
### Create Webhook
|
||||
```bash
|
||||
POST /api/notifications/webhooks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Webhook",
|
||||
"url": "https://example.com/webhook",
|
||||
"method": "POST",
|
||||
"service": "generic",
|
||||
"enabled": true,
|
||||
"template": "{\"alert\": \"{{.Message}}\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### Test Webhook
|
||||
```bash
|
||||
POST /api/notifications/webhooks/test
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Test",
|
||||
"url": "https://example.com/webhook",
|
||||
"service": "generic",
|
||||
"template": "{\"test\": true}"
|
||||
}
|
||||
```
|
||||
|
||||
### Update Webhook
|
||||
```bash
|
||||
PUT /api/notifications/webhooks/{id}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Updated Webhook",
|
||||
"url": "https://example.com/new-webhook",
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Webhook
|
||||
```bash
|
||||
DELETE /api/notifications/webhooks/{id}
|
||||
```
|
||||
|
||||
### List Webhooks
|
||||
```bash
|
||||
GET /api/notifications/webhooks
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Never expose webhook URLs publicly** - they often contain authentication tokens
|
||||
- **Use HTTPS URLs** when possible to encrypt data in transit
|
||||
- **Rotate webhook URLs periodically** if they contain embedded tokens
|
||||
- **Test webhooks carefully** to avoid sending test data to production channels
|
||||
- **Limit webhook permissions** in the receiving service where possible
|
||||
|
|
@ -23,13 +23,14 @@ interface WebhookConfigProps {
|
|||
export function WebhookConfig(props: WebhookConfigProps) {
|
||||
const [adding, setAdding] = createSignal(false);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [formData, setFormData] = createSignal<Omit<Webhook, 'id'> & { service: string }>({
|
||||
const [formData, setFormData] = createSignal<Omit<Webhook, 'id'> & { service: string; payloadTemplate?: string }>({
|
||||
name: '',
|
||||
url: '',
|
||||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
const [templates, setTemplates] = createSignal<WebhookTemplate[]>([]);
|
||||
const [showServiceDropdown, setShowServiceDropdown] = createSignal(false);
|
||||
|
|
@ -60,7 +61,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: data.method,
|
||||
headers: data.headers,
|
||||
enabled: data.enabled,
|
||||
service: data.service
|
||||
service: data.service,
|
||||
template: data.payloadTemplate
|
||||
};
|
||||
props.onAdd(newWebhook);
|
||||
// Reset form but keep adding state true
|
||||
|
|
@ -70,7 +72,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -84,7 +87,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -92,7 +96,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
setEditingId(webhook.id!);
|
||||
setFormData({
|
||||
...webhook,
|
||||
service: webhook.service || 'generic'
|
||||
service: webhook.service || 'generic',
|
||||
payloadTemplate: webhook.template || ''
|
||||
});
|
||||
setAdding(true);
|
||||
};
|
||||
|
|
@ -284,6 +289,33 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom Payload Template - only show for generic service */}
|
||||
<Show when={formData().service === 'generic'}>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Custom Payload Template (JSON)
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Optional - Leave empty to use default
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={formData().payloadTemplate || ''}
|
||||
onInput={(e) => setFormData({ ...formData(), payloadTemplate: e.currentTarget.value })}
|
||||
placeholder={`{
|
||||
"text": "Alert: {{.Level}} - {{.Message}}",
|
||||
"resource": "{{.ResourceName}}",
|
||||
"value": {{.Value}},
|
||||
"threshold": {{.Threshold}}
|
||||
}`}
|
||||
rows={8}
|
||||
class="w-full px-3 py-2 text-xs font-mono border rounded-lg dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Available variables: {"{{.ID}}, {{.Level}}, {{.Type}}, {{.ResourceName}}, {{.Node}}, {{.Message}}, {{.Value}}, {{.Threshold}}, {{.Duration}}, {{.Timestamp}}"}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@ interface WebhookConfigProps {
|
|||
export function WebhookConfig(props: WebhookConfigProps) {
|
||||
const [adding, setAdding] = createSignal(false);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [formData, setFormData] = createSignal<Omit<Webhook, 'id'> & { service: string }>({
|
||||
const [formData, setFormData] = createSignal<Omit<Webhook, 'id'> & { service: string; payloadTemplate?: string }>({
|
||||
name: '',
|
||||
url: '',
|
||||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
const [templates, setTemplates] = createSignal<WebhookTemplate[]>([]);
|
||||
const [showServiceDropdown, setShowServiceDropdown] = createSignal(false);
|
||||
|
|
@ -60,7 +61,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: data.method,
|
||||
headers: data.headers,
|
||||
enabled: data.enabled,
|
||||
service: data.service
|
||||
service: data.service,
|
||||
template: data.payloadTemplate
|
||||
};
|
||||
props.onAdd(newWebhook);
|
||||
// Reset form but keep adding state true
|
||||
|
|
@ -70,7 +72,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -84,7 +87,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
method: 'POST',
|
||||
service: 'generic',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
enabled: true
|
||||
enabled: true,
|
||||
payloadTemplate: ''
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -92,7 +96,8 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
setEditingId(webhook.id!);
|
||||
setFormData({
|
||||
...webhook,
|
||||
service: webhook.service || 'generic'
|
||||
service: webhook.service || 'generic',
|
||||
payloadTemplate: webhook.template || ''
|
||||
});
|
||||
setAdding(true);
|
||||
};
|
||||
|
|
@ -284,6 +289,33 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom Payload Template - only show for generic service */}
|
||||
<Show when={formData().service === 'generic'}>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Custom Payload Template (JSON)
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Optional - Leave empty to use default
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={formData().payloadTemplate || ''}
|
||||
onInput={(e) => setFormData({ ...formData(), payloadTemplate: e.currentTarget.value })}
|
||||
placeholder={`{
|
||||
"text": "Alert: {{.Level}} - {{.Message}}",
|
||||
"resource": "{{.ResourceName}}",
|
||||
"value": {{.Value}},
|
||||
"threshold": {{.Threshold}}
|
||||
}`}
|
||||
rows={8}
|
||||
class="w-full px-3 py-2 text-xs font-mono border rounded-lg dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Available variables: {"{{.ID}}, {{.Level}}, {{.Type}}, {{.ResourceName}}, {{.Node}}, {{.Message}}, {{.Value}}, {{.Threshold}}, {{.Duration}}, {{.Timestamp}}"}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -337,6 +337,11 @@ func (h *NotificationHandlers) TestWebhook(w http.ResponseWriter, r *http.Reques
|
|||
RetryEnabled: false, // Don't retry during testing
|
||||
}
|
||||
|
||||
// If the webhook has a custom template, use it
|
||||
if basicWebhook.Template != "" {
|
||||
webhook.PayloadTemplate = basicWebhook.Template
|
||||
}
|
||||
|
||||
// Try to extract service from body if present
|
||||
var serviceCheck struct {
|
||||
Service string `json:"service"`
|
||||
|
|
@ -354,23 +359,25 @@ func (h *NotificationHandlers) TestWebhook(w http.ResponseWriter, r *http.Reques
|
|||
Str("name", webhook.Name).
|
||||
Msg("Testing webhook")
|
||||
|
||||
// Get template for the service
|
||||
templates := notifications.GetWebhookTemplates()
|
||||
for _, tmpl := range templates {
|
||||
if tmpl.Service == webhook.Service {
|
||||
webhook.PayloadTemplate = tmpl.PayloadTemplate
|
||||
if webhook.Headers == nil {
|
||||
webhook.Headers = make(map[string]string)
|
||||
// Get template for the service (if not using custom template)
|
||||
if webhook.PayloadTemplate == "" {
|
||||
templates := notifications.GetWebhookTemplates()
|
||||
for _, tmpl := range templates {
|
||||
if tmpl.Service == webhook.Service {
|
||||
webhook.PayloadTemplate = tmpl.PayloadTemplate
|
||||
if webhook.Headers == nil {
|
||||
webhook.Headers = make(map[string]string)
|
||||
}
|
||||
for k, v := range tmpl.Headers {
|
||||
webhook.Headers[k] = v
|
||||
}
|
||||
log.Info().Str("service", webhook.Service).Msg("Found template for service")
|
||||
break
|
||||
}
|
||||
for k, v := range tmpl.Headers {
|
||||
webhook.Headers[k] = v
|
||||
}
|
||||
log.Info().Str("service", webhook.Service).Msg("Found template for service")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no template found, use a simple generic template
|
||||
// If still no template found, use a simple generic template
|
||||
if webhook.PayloadTemplate == "" {
|
||||
webhook.PayloadTemplate = `{
|
||||
"alert": {
|
||||
|
|
|
|||
|
|
@ -66,13 +66,14 @@ type EmailConfig struct {
|
|||
|
||||
// WebhookConfig holds webhook settings
|
||||
type WebhookConfig struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Service string `json:"service"` // discord, slack, teams, etc.
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Service string `json:"service"` // discord, slack, teams, etc.
|
||||
Template string `json:"template"` // Custom payload template
|
||||
}
|
||||
|
||||
// NewNotificationManager creates a new notification manager
|
||||
|
|
@ -404,8 +405,43 @@ func (n *NotificationManager) sendGroupedWebhook(webhook WebhookConfig, alertLis
|
|||
var jsonData []byte
|
||||
var err error
|
||||
|
||||
// For service-specific webhooks, use the first alert with a note about others
|
||||
if webhook.Service != "" && webhook.Service != "generic" && len(alertList) > 0 {
|
||||
// Check if webhook has a custom template first
|
||||
if webhook.Template != "" && len(alertList) > 0 {
|
||||
// Use custom template with the first alert (for simplicity)
|
||||
alert := alertList[0]
|
||||
if len(alertList) > 1 {
|
||||
alert.Message = fmt.Sprintf("%s (and %d more alerts)", alert.Message, len(alertList)-1)
|
||||
}
|
||||
|
||||
enhanced := EnhancedWebhookConfig{
|
||||
WebhookConfig: webhook,
|
||||
Service: webhook.Service,
|
||||
PayloadTemplate: webhook.Template,
|
||||
}
|
||||
|
||||
data := n.prepareWebhookData(alert, nil)
|
||||
|
||||
// For Telegram, extract chat_id from URL if present
|
||||
if webhook.Service == "telegram" && strings.Contains(webhook.URL, "chat_id=") {
|
||||
if u, err := url.Parse(webhook.URL); err == nil {
|
||||
chatID := u.Query().Get("chat_id")
|
||||
if chatID != "" {
|
||||
data.ChatID = chatID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err = n.generatePayloadFromTemplate(enhanced.PayloadTemplate, data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("webhook", webhook.Name).
|
||||
Int("alertCount", len(alertList)).
|
||||
Msg("Failed to generate grouped payload from custom template")
|
||||
return
|
||||
}
|
||||
} else if webhook.Service != "" && webhook.Service != "generic" && len(alertList) > 0 {
|
||||
// For service-specific webhooks, use the first alert with a note about others
|
||||
// For simplicity, send the first alert with a note about others
|
||||
// Most webhook services work better with single structured payloads
|
||||
alert := alertList[0]
|
||||
|
|
@ -502,7 +538,23 @@ func (n *NotificationManager) sendWebhookRequest(webhook WebhookConfig, jsonData
|
|||
method = "POST"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, webhook.URL, bytes.NewBuffer(jsonData))
|
||||
// For Telegram webhooks, strip chat_id from URL if present
|
||||
// The chat_id should only be in the JSON body, not the URL
|
||||
webhookURL := webhook.URL
|
||||
if webhook.Service == "telegram" && strings.Contains(webhookURL, "chat_id=") {
|
||||
if u, err := url.Parse(webhookURL); err == nil {
|
||||
q := u.Query()
|
||||
q.Del("chat_id") // Remove chat_id from query params
|
||||
u.RawQuery = q.Encode()
|
||||
webhookURL = u.String()
|
||||
log.Debug().
|
||||
Str("original", webhook.URL).
|
||||
Str("cleaned", webhookURL).
|
||||
Msg("Stripped chat_id from Telegram webhook URL")
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, webhookURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
|
|
@ -555,8 +607,39 @@ func (n *NotificationManager) sendWebhook(webhook WebhookConfig, alert *alerts.A
|
|||
var jsonData []byte
|
||||
var err error
|
||||
|
||||
// Check if this webhook has a service type and use the proper template
|
||||
if webhook.Service != "" && webhook.Service != "generic" {
|
||||
// Check if webhook has a custom template first
|
||||
if webhook.Template != "" {
|
||||
// Use custom template provided by user
|
||||
enhanced := EnhancedWebhookConfig{
|
||||
WebhookConfig: webhook,
|
||||
Service: webhook.Service,
|
||||
PayloadTemplate: webhook.Template,
|
||||
}
|
||||
|
||||
// Prepare data and generate payload
|
||||
data := n.prepareWebhookData(alert, nil)
|
||||
|
||||
// For Telegram, still extract chat_id from URL if present
|
||||
if webhook.Service == "telegram" && strings.Contains(webhook.URL, "chat_id=") {
|
||||
if u, err := url.Parse(webhook.URL); err == nil {
|
||||
chatID := u.Query().Get("chat_id")
|
||||
if chatID != "" {
|
||||
data.ChatID = chatID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err = n.generatePayloadFromTemplate(enhanced.PayloadTemplate, data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("webhook", webhook.Name).
|
||||
Str("alertID", alert.ID).
|
||||
Msg("Failed to generate webhook payload from custom template")
|
||||
return
|
||||
}
|
||||
} else if webhook.Service != "" && webhook.Service != "generic" {
|
||||
// Check if this webhook has a service type and use the proper template
|
||||
// Convert to enhanced webhook to use template
|
||||
enhanced := EnhancedWebhookConfig{
|
||||
WebhookConfig: webhook,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
|
|
@ -345,6 +347,16 @@ func (n *NotificationManager) TestEnhancedWebhook(webhook EnhancedWebhookConfig)
|
|||
// Prepare data
|
||||
data := n.prepareWebhookData(testAlert, webhook.CustomFields)
|
||||
|
||||
// For Telegram, extract chat_id from URL if present
|
||||
if webhook.Service == "telegram" && strings.Contains(webhook.URL, "chat_id=") {
|
||||
if u, err := url.Parse(webhook.URL); err == nil {
|
||||
chatID := u.Query().Get("chat_id")
|
||||
if chatID != "" {
|
||||
data.ChatID = chatID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate payload
|
||||
payload, err := n.generatePayloadFromTemplate(webhook.PayloadTemplate, data)
|
||||
if err != nil {
|
||||
|
|
@ -357,7 +369,18 @@ func (n *NotificationManager) TestEnhancedWebhook(webhook EnhancedWebhookConfig)
|
|||
method = "POST"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, webhook.URL, bytes.NewBuffer(payload))
|
||||
// For Telegram webhooks, strip chat_id from URL if present
|
||||
webhookURL := webhook.URL
|
||||
if webhook.Service == "telegram" && strings.Contains(webhookURL, "chat_id=") {
|
||||
if u, err := url.Parse(webhookURL); err == nil {
|
||||
q := u.Query()
|
||||
q.Del("chat_id") // Remove chat_id from query params
|
||||
u.RawQuery = q.Encode()
|
||||
webhookURL = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, webhookURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
"parse_mode": "Markdown",
|
||||
"disable_web_page_preview": true
|
||||
}`,
|
||||
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",
|
||||
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\n5. IMPORTANT: Do NOT include chat_id in the URL. Pulse will handle it automatically.",
|
||||
},
|
||||
{
|
||||
Service: "slack",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue