firehol/netdata

View on GitHub
src/web/gui/src/dashboard.js/alarms.js

Summary

Maintainability
F
2 wks
Test Coverage

// Registry of netdata hosts

NETDATA.alarms = {
    onclick: null,                  // the callback to handle the click - it will be called with the alarm log entry
    chart_div_offset: -50,          // give that space above the chart when scrolling to it
    chart_div_id_prefix: 'chart_',  // the chart DIV IDs have this prefix (they should be NETDATA.name2id(chart.id))
    chart_div_animation_duration: 0,// the duration of the animation while scrolling to a chart

    ms_penalty: 0,                  // the time penalty of the next alarm
    ms_between_notifications: 500,  // firefox moves the alarms off-screen (above, outside the top of the screen)
                                    // if alarms are shown faster than: one per 500ms

    update_every: 10000,            // the time in ms between alarm checks

    notifications: false,           // when true, the browser supports notifications (may not be granted though)
    last_notification_id: 0,        // the id of the last alarm_log we have raised an alarm for
    first_notification_id: 0,       // the id of the first alarm_log entry for this session
                                    // this is used to prevent CLEAR notifications for past events
    // notifications_shown: [],

    server: null,                   // the server to connect to for fetching alarms
    current: null,                  // the list of raised alarms - updated in the background

    // a callback function to call every time the list of raised alarms is refreshed
    callback: (typeof netdataAlarmsActiveCallback === 'function') ? netdataAlarmsActiveCallback : null,

    // a callback function to call every time a notification is shown
    // the return value is used to decide if the notification will be shown
    notificationCallback: (typeof netdataAlarmsNotifCallback === 'function') ? netdataAlarmsNotifCallback : null,

    recipients: null,               // the list (array) of recipients to show alarms for, or null

    recipientMatches: function (to_string, wanted_array) {
        if (typeof wanted_array === 'undefined' || wanted_array === null || Array.isArray(wanted_array) === false) {
            return true;
        }

        let r = ' ' + to_string.toString() + ' ';
        let len = wanted_array.length;
        while (len--) {
            if (r.indexOf(' ' + wanted_array[len] + ' ') >= 0) {
                return true;
            }
        }

        return false;
    },

    activeForRecipients: function () {
        let active = {};
        let data = NETDATA.alarms.current;

        if (typeof data === 'undefined' || data === null) {
            return active;
        }

        for (let x in data.alarms) {
            if (!data.alarms.hasOwnProperty(x)) {
                continue;
            }

            let alarm = data.alarms[x];
            if ((alarm.status === 'WARNING' || alarm.status === 'CRITICAL') && NETDATA.alarms.recipientMatches(alarm.recipient, NETDATA.alarms.recipients)) {
                active[x] = alarm;
            }
        }

        return active;
    },

    notify: function (entry) {
        // console.log('alarm ' + entry.unique_id);

        if (entry.updated) {
            // console.log('alarm ' + entry.unique_id + ' has been updated by another alarm');
            return;
        }

        let value_string = entry.value_string;

        if (NETDATA.alarms.current !== null) {
            // get the current value_string
            let t = NETDATA.alarms.current.alarms[entry.chart + '.' + entry.name];
            if (typeof t !== 'undefined' && entry.status === t.status && typeof t.value_string !== 'undefined') {
                value_string = t.value_string;
            }
        }

        let name = entry.name.replace(/_/g, ' ');
        let status = entry.status.toLowerCase();
        let title = name + ' = ' + value_string.toString();
        let tag = entry.alarm_id;
        let icon = 'images/banner-icon-144x144.png';
        let interaction = false;
        let data = entry;
        let show = true;

        // console.log('alarm ' + entry.unique_id + ' ' + entry.chart + '.' + entry.name + ' is ' +  entry.status);

        switch (entry.status) {
            case 'REMOVED':
                show = false;
                break;

            case 'UNDEFINED':
                return;

            case 'UNINITIALIZED':
                return;

            case 'CLEAR':
                if (entry.unique_id < NETDATA.alarms.first_notification_id) {
                    // console.log('alarm ' + entry.unique_id + ' is not current');
                    return;
                }
                if (entry.old_status === 'UNINITIALIZED' || entry.old_status === 'UNDEFINED') {
                    // console.log('alarm' + entry.unique_id + ' switch to CLEAR from ' + entry.old_status);
                    return;
                }
                if (entry.no_clear_notification) {
                    // console.log('alarm' + entry.unique_id + ' is CLEAR but has no_clear_notification flag');
                    return;
                }
                title = name + ' back to normal (' + value_string.toString() + ')';
                icon = 'images/check-mark-2-128-green.png';
                interaction = false;
                break;

            case 'WARNING':
                if (entry.old_status === 'CRITICAL') {
                    status = 'demoted to ' + entry.status.toLowerCase();
                }

                icon = 'images/alert-128-orange.png';
                interaction = false;
                break;

            case 'CRITICAL':
                if (entry.old_status === 'WARNING') {
                    status = 'escalated to ' + entry.status.toLowerCase();
                }

                icon = 'images/alert-128-red.png';
                interaction = true;
                break;

            default:
                console.log('invalid alarm status ' + entry.status);
                return;
        }

        // filter recipients
        if (show) {
            show = NETDATA.alarms.recipientMatches(entry.recipient, NETDATA.alarms.recipients);
        }

        /*
        // cleanup old notifications with the same alarm_id as this one
        // it does not seem to work on any web browser - so notifications cannot be removed

        let len = NETDATA.alarms.notifications_shown.length;
        while (len--) {
            let n = NETDATA.alarms.notifications_shown[len];
            if (n.data.alarm_id === entry.alarm_id) {
                console.log('removing old alarm ' + n.data.unique_id);

                // close the notification
                n.close.bind(n);

                // remove it from the array
                NETDATA.alarms.notifications_shown.splice(len, 1);
                len = NETDATA.alarms.notifications_shown.length;
            }
        }
        */

        if (show) {
            if (typeof NETDATA.alarms.notificationCallback === 'function') {
                show = NETDATA.alarms.notificationCallback(entry);
            }

            if (show) {
                setTimeout(function () {
                    // show this notification
                    // console.log('new notification: ' + title);
                    let n = new Notification(title, {
                        body: entry.hostname + ' - ' + entry.chart + ' (' + entry.family + ') - ' + status + ': ' + entry.info,
                        tag: tag,
                        requireInteraction: interaction,
                        icon: NETDATA.serverStatic + icon,
                        data: data
                    });

                    n.onclick = function (event) {
                        event.preventDefault();
                        NETDATA.alarms.onclick(event.target.data);
                    };

                    // console.log(n);
                    // NETDATA.alarms.notifications_shown.push(n);
                    // console.log(entry);
                }, NETDATA.alarms.ms_penalty);

                NETDATA.alarms.ms_penalty += NETDATA.alarms.ms_between_notifications;
            }
        }
    },

    scrollToChart: function (chart_id) {
        if (typeof chart_id === 'string') {
            let offset = $('#' + NETDATA.alarms.chart_div_id_prefix + NETDATA.name2id(chart_id)).offset();
            if (typeof offset !== 'undefined') {
                $('html, body').animate({scrollTop: offset.top + NETDATA.alarms.chart_div_offset}, NETDATA.alarms.chart_div_animation_duration);
                return true;
            }
        }
        return false;
    },

    scrollToAlarm: function (alarm) {
        if (typeof alarm === 'object') {
            let ret = NETDATA.alarms.scrollToChart(alarm.chart);

            if (ret && NETDATA.options.page_is_visible === false) {
                window.focus();
            }
            //    alert('netdata dashboard will now scroll to chart: ' + alarm.chart + '\n\nThis alarm opened to bring the browser window in front of the screen. Click on the dashboard to prevent it from appearing again.');
        }

    },

    notifyAll: function () {
        // console.log('FETCHING ALARM LOG');
        NETDATA.alarms.get_log(NETDATA.alarms.last_notification_id, function (data) {
            // console.log('ALARM LOG FETCHED');

            if (data === null || typeof data !== 'object') {
                console.log('invalid alarms log response');
                return;
            }

            if (data.length === 0) {
                console.log('received empty alarm log');
                return;
            }

            // console.log('received alarm log of ' + data.length + ' entries, from ' + data[data.length - 1].unique_id.toString() + ' to ' + data[0].unique_id.toString());

            data.sort(function (a, b) {
                if (a.unique_id > b.unique_id) {
                    return -1;
                }
                if (a.unique_id < b.unique_id) {
                    return 1;
                }
                return 0;
            });

            NETDATA.alarms.ms_penalty = 0;

            let len = data.length;
            while (len--) {
                if (data[len].unique_id > NETDATA.alarms.last_notification_id) {
                    NETDATA.alarms.notify(data[len]);
                }
                //else
                //    console.log('ignoring alarm (older) with id ' + data[len].unique_id.toString());
            }

            NETDATA.alarms.last_notification_id = data[0].unique_id;

            if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) {
                NETDATA.localStorageSet('last_notification_id', NETDATA.alarms.last_notification_id, null);
            }
            // console.log('last notification id = ' + NETDATA.alarms.last_notification_id);
        })
    },

    check_notifications: function () {
        // returns true if we should fire 1+ notifications

        if (NETDATA.alarms.notifications !== true) {
            // console.log('web notifications are not available');
            return false;
        }

        if (Notification.permission !== 'granted') {
            // console.log('web notifications are not granted');
            return false;
        }

        if (typeof NETDATA.alarms.current !== 'undefined' && typeof NETDATA.alarms.current.alarms === 'object') {
            // console.log('can do alarms: old id = ' + NETDATA.alarms.last_notification_id + ' new id = ' + NETDATA.alarms.current.latest_alarm_log_unique_id);

            if (NETDATA.alarms.current.latest_alarm_log_unique_id > NETDATA.alarms.last_notification_id) {
                // console.log('new alarms detected');
                return true;
            }
            //else console.log('no new alarms');
        }
        // else console.log('cannot process alarms');

        return false;
    },

    get: function (what, callback) {
        $.ajax({
            url: NETDATA.alarms.server + '/api/v1/alarms?' + what.toString(),
            async: true,
            cache: false,
            headers: {
                'Cache-Control': 'no-cache, no-store',
                'Pragma': 'no-cache'
            },
            xhrFields: {withCredentials: true} // required for the cookie
        })
            .done(function (data) {
                data = NETDATA.xss.checkOptional('/api/v1/alarms', data /*, '.*\.(calc|calc_parsed|warn|warn_parsed|crit|crit_parsed)$' */);

                if (NETDATA.alarms.first_notification_id === 0 && typeof data.latest_alarm_log_unique_id === 'number') {
                    NETDATA.alarms.first_notification_id = data.latest_alarm_log_unique_id;
                }

                if (typeof callback === 'function') {
                    return callback(data);
                }
            })
            .fail(function () {
                NETDATA.error(415, NETDATA.alarms.server);

                if (typeof callback === 'function') {
                    return callback(null);
                }
            });
    },

    update_forever: function () {
        if (netdataShowAlarms !== true || netdataSnapshotData !== null) {
            return;
        }

        NETDATA.alarms.get('active', function (data) {
            if (data !== null) {
                NETDATA.alarms.current = data;

                if (NETDATA.alarms.check_notifications()) {
                    NETDATA.alarms.notifyAll();
                }

                if (typeof NETDATA.alarms.callback === 'function') {
                    NETDATA.alarms.callback(data);
                }

                // Health monitoring is disabled on this netdata
                if (data.status === false) {
                    return;
                }
            }

            setTimeout(NETDATA.alarms.update_forever, NETDATA.alarms.update_every);
        });
    },

    get_log: function (last_id, callback) {
        // console.log('fetching all log after ' + last_id.toString());
        $.ajax({
            url: NETDATA.alarms.server + '/api/v1/alarm_log?after=' + last_id.toString(),
            async: true,
            cache: false,
            headers: {
                'Cache-Control': 'no-cache, no-store',
                'Pragma': 'no-cache'
            },
            xhrFields: {withCredentials: true} // required for the cookie
        })
            .done(function (data) {
                data = NETDATA.xss.checkOptional('/api/v1/alarm_log', data);

                if (typeof callback === 'function') {
                    return callback(data);
                }
            })
            .fail(function () {
                NETDATA.error(416, NETDATA.alarms.server);

                if (typeof callback === 'function') {
                    return callback(null);
                }
            });
    },

    init: function () {
        NETDATA.alarms.server = NETDATA.fixHost(NETDATA.serverDefault);

        if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) {
            NETDATA.alarms.last_notification_id =
                NETDATA.localStorageGet('last_notification_id', NETDATA.alarms.last_notification_id, null);
        }

        if (NETDATA.alarms.onclick === null) {
            NETDATA.alarms.onclick = NETDATA.alarms.scrollToAlarm;
        }

        if (typeof netdataAlarmsRecipients !== 'undefined' && Array.isArray(netdataAlarmsRecipients)) {
            NETDATA.alarms.recipients = netdataAlarmsRecipients;
        }

        if (netdataShowAlarms) {
            NETDATA.alarms.update_forever();

            if ('Notification' in window) {
                // console.log('notifications available');
                NETDATA.alarms.notifications = true;

                if (Notification.permission === 'default') {
                    Notification.requestPermission();
                }
            }
        }
    }
};