Zabbix Integration Questions

Information:

  • Zammad version used: 6.5.0-1751526843.c85c50b7.noble
  • Zammad installation type used: package
  • Operating system: Ubuntu Server 24.04
  • Browser + version: Google Chrome - 138.0.7204.101

Expected behavior:

Integration with monitoring via Zabbix. When a failure event is generated, the system receives an alert, opening a ticket. After the issue is resolved, the ticket status changes to closed. Additionally, when updating the issue via a note in Zammad, this update is also included within the Zabbix event, and vice versa. Vice versa.

Actual behavior:

I performed the integration following the manual, following these documents: Zabbix Integration — Zammad Admin Documentation documentation and Zammad monitoring and integration with Zabbix. After that, when an alert is generated in Zabbix, it interacts with Zammad, opening the ticket with the correct information. Everything is fine. And when I update the issue in Zabbix, it adds a new note to the ticket history within Zammad, everything is fine. However, when we do this within Zammad and add a note, it doesn’t feed that information into Zabbix. And another point: when the issue is resolved, Zabbix triggers the information. Zammad can capture it and also feed it in the ticket history as a note. However, the ticket status remains open, even after receiving the validation information that Zabbix envied.

Steps to reproduce the behavior:

In order to have the tickets auto close when zabbix sends the resolved note you need to have a trigger created on the Zammad side. Something like

-Customer - specific user - (your zabbix user)
-Action is updated
-text contains “Problem resolved in”

Execute Changes
-State closed

This will auto close the ticket.

For the issue of updating in zammad and it reflecting in Zabbix, the integration is one way. Zabbix uses the Zammad API key to send tickets directly into the system. There is no line of communication in the other direction.

1.120 / 5.000
Perfect, I hadn’t considered creating a trigger in Zammad to automate this.

Regarding unilateral communication, I had kind of understood it would be like this. I just wanted to confirm with colleagues who have more experience. In our scenario, we currently have some 24x7 environments where it would be ideal to keep the Zabbix side of the system open to on-call staff. In other words, if Zammad could feed the Zabbix serial port, IDEAL. Currently, in these environments, with OTOBO (formerly OTRS), we can achieve this bilateral communication.

How is OTRS communicating to Zabbix?

I’m looking at the OTRS integration and it looks basically identical to the Zammad integration (a web hook that pushes data to a ticket).

I feel like whatever you currently have in place is a custom job inside of OTRS to push information to Zabbix.

Honestly, do yourself a favor and do not use the suggested Zabbix way (the documentation on the Zammad docs also originate by them), but use the Check_MK documentation and write an API media script for it:

No triggers needed, it just works.

If I’d have to guess, OTRS pushes data via maybe webhook back to Zabbix. Back in the days when the “integration” was written, this was not supported by Zammad at all.

Hello, good afternoon.

I actually used the documentation available in Zabbix and on the Zammad portal as a basis for the integration we currently have. I’ll carefully read the one you shared now.

I have a question. If you could help me, would it be possible to create a script so that when a problem is identified in Zabbix and a ticket is created in Zammad, Zammad only sends the ticket number to Zabbix so it can record it? I think this interaction alone would be valid. I wouldn’t want to replicate the other updates made within Zammad within Zabbix, especially since it’s not a ticketing system.

Thank you!

Hello,

in my test instance I tried something similar.

For this test:

  • I created a ticket object called zabbix_eventid

  • This is The media type setup in Zabbix

  • The Zabbix Event ID is recorded in the ticket itself using this script in the Zabbix media type for Zammad

var Zammad = {
    params: {},

    setParams: function (params) {
        if (typeof params !== 'object') {
            return;
        }

        Zammad.params = params;
        if (typeof Zammad.params.url === 'string') {
            if (!Zammad.params.url.endsWith('/')) {
                Zammad.params.url += '/';
            }
        }
    },

    request: function (method, query, data) {
        ['url', 'access_token'].forEach(function (field) {
            if (typeof Zammad.params !== 'object' || typeof Zammad.params[field] === 'undefined'
                || Zammad.params[field] === '' ) {
                throw 'Required param is not set: "' + field + '".';
            }
        });

        var response,
            url = Zammad.params.url + query,
            request = new HttpRequest();

        if (typeof Zammad.HTTPProxy === 'string' && Zammad.HTTPProxy.trim() !== '') {
            request.setProxy(Zammad.HTTPProxy);
        }

        request.addHeader('Content-Type: application/json');
        request.addHeader('Authorization: Token token=' + Zammad.params.access_token);

        if (typeof data !== 'undefined') {
            data = JSON.stringify(data);
        }

        Zabbix.log(4, '[ Zammad Webhook ] Sending request: ' +
            url + ((typeof data === 'string') ? (' ' + data) : ''));

        switch (method) {
            case 'get':
                response = request.get(url, data);
                break;

            case 'post':
                response = request.post(url, data);
                break;

            case 'put':
                response = request.put(url, data);
                break;

            default:
                throw 'Unsupported HTTP request method: ' + method;
        }

        Zabbix.log(4, '[ Zammad Webhook ] Received response with status code ' + request.getStatus() + ': ' + response);

        if (response !== null) {
            try {
                response = JSON.parse(response);
            }
            catch (error) {
                Zabbix.log(4, '[ Zammad Webhook ] Failed to parse response received from Zammad');
                response = null;
            }
        }

        if (request.getStatus() < 200 || request.getStatus() >= 300) {
            var message = 'Request failed with status code ' + request.getStatus();

            if (response !== null && typeof response.errors !== 'undefined'
                && Object.keys(response.errors).length > 0) {
                message += ': ' + JSON.stringify(response.errors);
            }
            else if (response !== null && typeof response.errorMessages !== 'undefined'
                && Object.keys(response.errorMessages).length > 0) {
                message += ': ' + JSON.stringify(response.errorMessages);
            }

            throw message + ' Check debug log for more information.';
        }

        return {
            status: request.getStatus(),
            response: response
        };
    },

    setTicketTags: function (tags, ticket_id) {
        var data = {
            item: '',
            object: 'Ticket',
            o_id: ticket_id
        };

        try {
            var tags_json = JSON.parse(tags),
                result;

            for (var i in tags_json) {

                if (tags_json[i].value) {
                    data.item = tags_json[i].tag + ": " + tags_json[i].value;
                } else {
                    data.item = tags_json[i].tag;
                }
                result = Zammad.request('post', 'api/v1/tags/add', data);

                if (typeof result.response !== 'object' || result.status != 200) {
                    Zabbix.log(4, '[ Zammad Webhook ] Cannot add ticket tag:' + tags_json[i].tag);
                }
            }
        }
        catch (error) {
            Zabbix.log(4, '[ Zammad Webhook ] Failed to add ticket tags:' + error);
        }

        return;
    },

    createTicket: function(subject, message, priority, zabbix_eventid) {
        var data = {
            title: subject,
            group: 'Postmaster',
            article: {
                body: message,
                type: 'note',
                internal: false
            },
            customer: Zammad.params.customer
        };

        if (priority) {
            data.priority_id = priority;
        }

        // Add zabbix_eventid if provided
        if (zabbix_eventid) {
            data.zabbix_eventid = zabbix_eventid;
        }

        var result = Zammad.request('post', 'api/v1/tickets', data);

        if (typeof result.response !== 'object'
            || typeof result.response.id === 'undefined'
            || result.status != 201) {
            throw 'Cannot create Zammad ticket. Check debug log for more information.';
        }

        return result.response.id;
    },

    updateTicket: function(subject, message) {
        var data = {
            ticket_id: Zammad.params.ticket_id,
            body: message || '',
            type: 'note',
            internal: false
        };

        var result = Zammad.request('post', 'api/v1/ticket_articles', data);

        if (typeof result.response !== 'object'
            || typeof result.response.id === 'undefined'
            || result.status != 201) {
            throw 'Cannot update Zammad ticket. Check debug log for more information.';
        }
    }
};

try {
    var params = JSON.parse(value),
        params_zammad = {},
        params_update = {},
        result = {tags: {}},
        required_params = [
            'alert_subject', 'customer',
            'event_source', 'event_value',
            'event_update_status'
        ],
        severities = [
            {name: 'not_classified', color: '#97AAB3'},
            {name: 'information', color: '#7499FF'},
            {name: 'warning', color: '#FFC859'},
            {name: 'average', color: '#FFA059'},
            {name: 'high', color: '#E97659'},
            {name: 'disaster', color: '#E45959'},
            {name: 'resolved', color: '#009900'},
            {name: null, color: '#000000'}
        ],
        priority;

    Object.keys(params)
        .forEach(function (key) {
            if (key.startsWith('zammad_')) {
                params_zammad[key.substring(7)] = params[key].trim();
            }
            else if (key.startsWith('event_update_')) {
                params_update[key.substring(13)] = params[key];
            }
            else if (required_params.indexOf(key) !== -1 && params[key].trim() === '') {
                throw 'Parameter "' + key + '" cannot be empty.';
            }
        });

    if ([0, 1, 2, 3].indexOf(parseInt(params.event_source)) === -1) {
        throw 'Incorrect "event_source" parameter given: ' + params.event_source + '\nMust be 0-3.';
    }

    // Check {EVENT.VALUE} for trigger-based and internal events.
    if (params.event_value !== '0' && params.event_value !== '1'
        && (params.event_source === '0' || params.event_source === '3')) {
        throw 'Incorrect "event_value" parameter given: ' + params.event_value + '\nMust be 0 or 1.';
    }

    // Check {EVENT.UPDATE.STATUS} only for trigger-based events.
    if (params.event_source === '0' && params.event_update_status !== '0' && params.event_update_status !== '1') {
        throw 'Incorrect "event_update_status" parameter given: ' + params.event_update_status + '\nMust be 0 or 1.';
    }

    if (params.event_source !== '0' && params.event_value === '0') {
        throw 'Recovery operations are supported only for trigger-based actions.';
    }

    if (params.event_source === '0'
        && ((params.event_value === '1' && params.event_update_status === '1')
            || (params.event_value === '0'
                && (params.event_update_status === '0' || params.event_update_status === '1')))
        && (isNaN(parseInt(params.zammad_ticket_id)) || parseInt(params.zammad_ticket_id) < 1 )) {
        throw 'Incorrect "zammad_ticket_id" parameter given: ' + params.zammad_ticket_id +
            '\nMust be positive integer.';
    }

    if ([0, 1, 2, 3, 4, 5].indexOf(parseInt(params.event_nseverity)) === -1) {
        params.event_nseverity = '7';
    }

    if (params.event_value === '0') {
        params.event_nseverity = '6';
    }

    priority = params['severity_' + severities[params.event_nseverity].name];
    priority = priority && priority.trim() || severities[7].name;

    Zammad.setParams(params_zammad);
    Zammad.HTTPProxy = params.HTTPProxy;

    // Create ticket for non trigger-based events.
    if (params.event_source !== '0'
        && params.event_value !== '0') {
        Zammad.createTicket(params.alert_subject, params.alert_message, priority, params.event_id);
    }
    // Create ticket for trigger-based events.
    else if (params.event_value === '1' && params_update.status === '0'
             && Number.isInteger(parseInt(Zammad.params.ticket_id)) === false) {
            var ticket_id = Zammad.createTicket(
                params.alert_subject,
                params.alert_subject + '\n' + params.alert_message + '\n' +
                    params.zabbix_url + (params.zabbix_url.endsWith('/') ? '' : '/') +
                    'tr_events.php?triggerid=' + params.trigger_id + '&eventid=' + params.event_id + '\n',
                priority,
                params.event_id // this sets `zabbix_eventid`
            );


        result.tags.__zbx_zammad_ticket_id = ticket_id;
        result.tags.__zbx_zammad_ticketlink = params.zammad_url +
            (params.zammad_url.endsWith('/') ? '' : '/') + '#ticket/zoom/' + ticket_id;

        if (Zammad.params.enable_tags.toLowerCase() === 'true') {
            Zammad.setTicketTags(params.event_tags, ticket_id);
        }
    }
    // Update created ticket for trigger-based event.
    else {
        Zammad.updateTicket(params.alert_subject, params.alert_message);
    }

    return JSON.stringify(result);
}
catch (error) {
    Zabbix.log(3, '[ Zammad Webhook ] ERROR: ' + error);
    throw 'Sending failed: ' + error;
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "event.acknowledge",
  "params": {
    "eventids": "#{ticket.zabbix_eventid}",
    "action": 6,
    "message": "Acknowledged by #{ticket.owner.firstname} #{ticket.owner.lastname} via Zammad"
  }
}

As far as I remember (it’s been a while now), this is how it worked.

Currently, I’m focusing on the Zabbix setup. Over the next few weeks, I’ll attempt to implement it in our production environment. I’m still exploring whether I should add additional custom fields to the ticket system, such as single-selection or multi-selection options, to improve two-way communication between Zabbix and Zammad—at least to some extent.

If anyone has better ideas or suggestions, I’m all ears!

Best,
Skip