feat(notifications): add Zabbix history.push provider (#1419)

* feat(notifications): add Zabbix history.push provider

* test(notifications): add Zabbix provider tests
This commit is contained in:
Sergei Nesterkov authored and GitHub committed 2026-08-17 15:24:47 +02:00
1 parent 8f29216701
commit 7a007bd623
6 files changed
+374 -6

No files matched your search

+11 -2
View File
@@ -40,6 +40,7 @@ import { sendApprise } from './apprise';
import { sendPushover } from './pushover';
import { sendGenericWebhook } from './generic-webhook';
import { sendWorkflows } from './workflows';
import { sendZabbix } from './zabbix';
// Send to every URL in an Apprise channel. Errors are aggregated so a single
// bad URL doesn't silently mask a healthy one.
@@ -110,6 +111,9 @@ async function sendToAppriseUrl(url: string, payload: NotificationPayload): Prom
return await sendGenericWebhook(url, payload);
case 'workflows':
return await sendWorkflows(url, payload);
case 'zabbix':
case 'zabbixs':
return await sendZabbix(url, payload);
default:
return { success: false, error: `Unsupported Apprise protocol: ${protocol}` };
}
@@ -149,7 +153,8 @@ export async function testNotification(setting: NotificationSettingData): Promis
const payload: NotificationPayload = {
title: 'Dockhand Test Notification',
message: 'This is a test notification from Dockhand. If you receive this, your notification settings are configured correctly.',
type: 'info'
type: 'info',
eventType: 'test'
};
if (setting.type === 'smtp') {
@@ -214,6 +219,7 @@ export async function sendEnvironmentNotification(
const enrichedPayload: NotificationPayload = {
...payload,
eventType,
environmentId,
environmentName: env.name
};
@@ -251,7 +257,10 @@ export async function sendEventNotification(
payload: NotificationPayload,
environmentId?: number
): Promise<{ success: boolean; sent: number }> {
let enrichedPayload = { ...payload };
let enrichedPayload: NotificationPayload = {
...payload,
eventType
};
if (environmentId) {
const env = await getEnvironment(environmentId);
if (env) {
+1
View File
@@ -14,6 +14,7 @@ export interface NotificationPayload {
type?: 'info' | 'success' | 'warning' | 'error';
environmentId?: number;
environmentName?: string;
eventType?: string;
}
export interface NotificationResult {
+180
View File
@@ -0,0 +1,180 @@
import {
notificationFetch,
type NotificationPayload,
type NotificationResult
} from './shared';
interface ZabbixApiResponse {
jsonrpc?: string;
result?: {
response?: string;
data?: Array<{
itemid?: string;
error?: string;
}>;
};
error?: {
code?: number;
message?: string;
data?: string;
};
id?: number;
}
export async function sendZabbix(
zabbixUrl: string,
payload: NotificationPayload
): Promise<NotificationResult> {
try {
const secure = /^zabbixs:\/\//i.test(zabbixUrl);
const httpUrl = zabbixUrl.replace(
/^zabbixs?:\/\//i,
secure ? 'https://' : 'http://'
);
const url = new URL(httpUrl);
const token = url.searchParams.get('token');
const host = url.searchParams.get('host');
const key = url.searchParams.get('key');
if (!token) {
return {
success: false,
error: 'Zabbix API token is required'
};
}
if (!host) {
return {
success: false,
error: 'Zabbix host is required'
};
}
if (!key) {
return {
success: false,
error: 'Zabbix item key is required'
};
}
/*
* These parameters belong to Dockhand's Zabbix URL syntax,
* not to the actual Zabbix API URL.
*/
url.searchParams.delete('token');
url.searchParams.delete('host');
url.searchParams.delete('key');
/*
* Allow shorthand:
*
* zabbixs://zabbix.example.com?...
*
* instead of requiring /api_jsonrpc.php explicitly.
*/
if (!url.pathname || url.pathname === '/') {
url.pathname = '/api_jsonrpc.php';
}
const event = {
event_type: payload.eventType ?? null,
title: payload.title,
message: payload.message,
type: payload.type ?? 'info',
environment: payload.environmentName ?? null,
timestamp: new Date().toISOString()
};
const requestBody = {
jsonrpc: '2.0',
method: 'history.push',
params: [
{
host,
key,
value: JSON.stringify(event)
}
],
id: 1
};
const response = await notificationFetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json-rpc',
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
});
const responseText = await response.text();
if (!response.ok) {
return {
success: false,
error: `Zabbix HTTP error ${response.status}: ${
responseText || response.statusText
}`
};
}
let result: ZabbixApiResponse;
try {
result = JSON.parse(responseText);
} catch {
return {
success: false,
error: 'Zabbix returned an invalid JSON response'
};
}
if (result.error) {
const parts = [
result.error.message,
result.error.data
].filter(Boolean);
return {
success: false,
error: `Zabbix API error: ${parts.join(' - ')}`
};
}
if (result.result?.response !== 'success') {
return {
success: false,
error: 'Zabbix history.push did not return success'
};
}
const itemErrors =
result.result.data
?.filter(item => item.error)
.map(item => item.error!) ?? [];
if (itemErrors.length > 0) {
return {
success: false,
error: `Zabbix history.push error: ${itemErrors.join('; ')}`
};
}
return {
success: true
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? `Zabbix connection failed: ${error.message}`
: `Zabbix connection failed: ${String(error)}`
};
}
}