Source: component/logging.js

/*! meta-admin/modules/logging */
/*jslint
    browser, long
*/
/*global
*/
/**
 * logging component.
 *
 * @module meta-admin/modules/component/logging
 */
import _ from "underscore";
import dates from "util-web/modules/dates";
import Highcharts from "highcharts";
import i18next from "util-web/modules/i18next";
import keyval from "util-web/modules/keyval";
import ko from "knockout";
import Logger from "js-logger";
import logging from "meta-client/modules/logging";
import model from "meta-core/modules/model";
import nav from "util-web/modules/nav";
import objects from "meta-client/modules/objects";
import protocol from "meta-core/modules/protocol";
import template from "./logging.html";
import ui from "util-web/modules/ui";

const LOG = Logger.get("meta-admin/logging");
const DEFAULT_METRIC_FILTER_PRESETS = [
    {
        filters: [
            "metrics.timers[meta-admin.editor.search]",
            "metrics.timers[meta-admin.logging.search]"
        ],
        name: "Admin"
    },
    {
        filters: [
            "metrics.meters[ch.rswk.meta.engine.ws.EngineWebSocketCreator.accepted]",
            "metrics.meters[ch.rswk.meta.engine.ws.EngineWebSocketCreator.refused]",
            "metrics.meters[ch.rswk.meta.engine.ws.EngineWebSocketListener.abnormalClose]",
            "metrics.meters[ch.rswk.meta.engine.ws.EngineWebSocketListener.error]",
            "metrics.meters[ch.rswk.meta.engine.ws.EngineWebSocketListener.requestError]",
            "metrics.meters[org.eclipse.jetty.servlet.ServletContextHandler.1xx-responses]",
            "metrics.meters[org.eclipse.jetty.servlet.ServletContextHandler.2xx-responses]",
            "metrics.meters[org.eclipse.jetty.servlet.ServletContextHandler.3xx-responses]",
            "metrics.meters[org.eclipse.jetty.servlet.ServletContextHandler.4xx-responses]",
            "metrics.timers[ch.rswk.meta.engine.ws.EngineWebSocketListener.request]",
            "metrics.timers[org.eclipse.jetty.servlet.ServletContextHandler.requests]"
        ],
        name: "Jetty"
    },
    {
        filters: [
            "metrics.counters[ch.rswk.meta.engine.EngineRuntime.Executor.running]",
            "metrics.counters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.running]",
            "metrics.counters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.scheduled.overrun]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.pool.core]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.pool.max]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.pool.size]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.tasks.active]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.tasks.capacity]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.tasks.completed]",
            "metrics.gauges[ch.rswk.meta.engine.EngineRuntime.Executor.tasks.queued]",
            "metrics.gauges[rt.cpu.availableProcessors]",
            "metrics.gauges[rt.cpu.processLoad]",
            "metrics.gauges[rt.cpu.systemLoad]",
            "metrics.gauges[rt.cpu.time]",
            "metrics.gauges[rt.memory.heap.committed]",
            "metrics.gauges[rt.memory.heap.init]",
            "metrics.gauges[rt.memory.heap.max]",
            "metrics.gauges[rt.memory.heap.usage]",
            "metrics.gauges[rt.memory.heap.used]",
            "metrics.gauges[rt.memory.total.committed]",
            "metrics.gauges[rt.memory.total.init]",
            "metrics.gauges[rt.memory.total.max]",
            "metrics.gauges[rt.memory.total.used]",
            "metrics.meters[ch.rswk.meta.engine.EngineEventBus.post]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.Executor.completed]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.Executor.submitted]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.completed]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.scheduled.once]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.scheduled.repetitively]",
            "metrics.meters[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.submitted]",
            "metrics.timers[ch.rswk.meta.engine.EngineRuntime.Executor.duration]",
            "metrics.timers[ch.rswk.meta.engine.EngineRuntime.Executor.idle]",
            "metrics.timers[ch.rswk.meta.engine.EngineRuntime.ScheduledExecutor.duration]"
        ],
        name: "System"
    }
];
const ENTRY_COLORS = ["#0000CD", "#008000", "#008B8B", "#6B8E23", "#800000", "#8B008B", "#A0522D", "#C71585", "#B22222", "#FF7F50"];
const ENTRY_LIMIT = 500;
const METER_PROPERTIES = ["count", "m15_rate", "m1_rate", "m5_rate", "mean_rate"];
const SEARCH_QUERY_KEY = "logging-query";
const TIMER_PROPERTIES = ["max", "mean", "min", "p50", "p75", "p95", "p98", "p99", "p999", "stddev"];

let entryColorIndex = 0;
const getNextEntryColor = function () {
    const color = ENTRY_COLORS[entryColorIndex];
    entryColorIndex = entryColorIndex + 1;
    if (ENTRY_COLORS.length <= entryColorIndex) {
        entryColorIndex = 0;
    }
    return color;
};
const entryColorMap = new Map();
const getEntryColor = function (clientId) {
    if (clientId) {
        if (entryColorMap.has(clientId) === false) {
            entryColorMap.set(clientId, getNextEntryColor());
        }
        return entryColorMap.get(clientId);
    }
};
const newChartOptions = function (id, title, series) {
    return {
        id,
        legend: {
            align: "left",
            itemDistance: 10,
            margin: 20
        },
        series,
        title: {
            align: "left",
            text: title
        },
        xAxis: {
            type: "datetime"
        },
        yAxis: {
            title: {
                text: ""
            }
        }
    };
};
const newMeterMap = function () {
    const map = new Map();
    METER_PROPERTIES.forEach((p) => map.set(p, []));
    return map;
};
const newTimerMap = function () {
    const map = newMeterMap();
    TIMER_PROPERTIES.forEach((p) => map.set(p, []));
    return map;
};

export default Object.freeze({
    template,
    viewModel: {
        createViewModel: function ({viewmodel}) {
            const searchTimer = logging.newMetricTimer(viewmodel.client, "meta-admin.logging.search");
            const vm = {};

            let metricsLiveInterval;

            vm.viewmodel = viewmodel;
            vm.metricFilterPresets = DEFAULT_METRIC_FILTER_PRESETS.concat(vm.viewmodel.config.logging.metricFilterPresets);

            vm.activeTab = ko.observable();
            vm.errorReply = ko.observable();

            vm.entryQuery = ko.observable();
            vm.entryFrom = ko.observable();
            vm.entryTo = ko.observable();
            vm.entryFromSub = dates.insertDateTimes({
                fromDateObservable: vm.entryFrom,
                toDateObservable: vm.entryTo
            });
            vm.entrySearcher = objects({
                searchReplyObjectsPath: "entries",
                toVm: (entry) => vm.viewmodel.toLoggingEntryVm(entry, getEntryColor)
            });
            vm.entryFilter = ko.observable();
            vm.entryFilterInclude = ko.observable(true);
            vm.entryColorMap = new Map();
            vm.filterComputed = ko.computed(function () {
                const filter = vm.entryFilter();
                const includeFilter = vm.entryFilterInclude();
                const hasFilter = _.isEmpty(filter) === false;
                vm.entrySearcher.objects().forEach(function (entry) {
                    const hideEntry = hasFilter && entry.message().indexOf(filter) < 0;
                    entry.hide(
                        includeFilter
                        ? hideEntry
                        : !hideEntry
                    );
                });
            });
            vm.filterComputed.extend({rateLimit: 500});

            vm.searchEntriesInternal = function (limit, clearSearch) {
                let query = vm.entryQuery();
                if (_.isEmpty(query)) {
                    query = i18next.t("logging_default_query");
                    vm.entryQuery(query);
                }
                LOG.debug(`searchEntriesInternal, query=${query}`);
                if (clearSearch) {
                    vm.entrySearcher.clear();
                }
                const from = dates.parseDateTime(vm.entryFrom());
                const to = dates.parseDateTime(vm.entryTo());
                let dateRangeQuery = "";
                if (from && to) {
                    dateRangeQuery = ` AND ${model.lucene.LOGGING_FIELD_TIMESTAMP}:${model.lucene.newDateTimeRangeQuery(from, to)}`;
                }
                let timerContext;
                return searchTimer.time().then(function (context) {
                    timerContext = context;
                    const searchRequest = protocol.loggingEntrySearchRequest({
                        cursor: vm.entrySearcher.searchCursor(),
                        limit,
                        query: query + dateRangeQuery
                    });
                    return vm.entrySearcher.processSearch(vm.viewmodel.client.loggingEntrySearch(searchRequest));
                }).then(function () {
                    vm.errorReply(undefined);
                    return keyval.set(SEARCH_QUERY_KEY, [query, vm.entryFrom(), vm.entryTo()].join("|"));
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    return timerContext.stop();
                });
            };

            vm.searchEntries = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                return vm.searchEntriesInternal(ENTRY_LIMIT, true).then(function () {
                    ui.resetForm(form);
                });
            };

            vm.loadMoreEntries = () => vm.searchEntriesInternal(ENTRY_LIMIT, false);

            vm.loadAllEntries = () => vm.searchEntriesInternal(0, false);

            vm.showDetails = (entryVm) => entryVm.showDetails(entryVm.showDetails() === false);

            vm.searchThread = function (entryVm) {
                vm.entryQuery(`thread:"${entryVm.threadName()}"`);
                return vm.searchEntriesInternal(ENTRY_LIMIT, true);
            };

            vm.searchLogger = function (entryVm) {
                vm.entryQuery(`logger:"${entryVm.loggerName()}"`);
                return vm.searchEntriesInternal(ENTRY_LIMIT, true);
            };

            vm.searchProperty = function (property) {
                vm.entryQuery(`${property.key}:"${property.value}"`);
                return vm.searchEntriesInternal(ENTRY_LIMIT, true);
            };

            vm.metricsFrom = ko.observable();
            vm.metricsTo = ko.observable();
            vm.metricsFromSub = dates.insertDateTimes({
                fromDateObservable: vm.metricsFrom,
                toDateObservable: vm.metricsTo
            });
            vm.metricsLimit = ko.observable(100);
            vm.metricsSampling = ko.observable(10);
            vm.metricsFilter = ko.observable();
            vm.metricFilterPreset = ko.observable();
            vm.isMetricsLive = ko.observable(false);
            vm.isLoadingMetrics = ko.observable();
            vm.healthchecks = ko.observableArray();
            vm.meterChartNames = ko.observableArray();
            vm.timerChartNames = ko.observableArray();

            vm.metricFilterPreset.subscribe(function (filters) {
                if (_.isEmpty(filters) === false) {
                    vm.metricsFilter(filters.join(","));
                } else {
                    vm.metricsFilter(undefined);
                }
            });

            vm.forgeMetricsChartSeries = function (results) {
                const healthchecksSeriesMap = new Map();
                const counterSeriesMap = new Map();
                const gaugeSeriesMap = new Map();
                const meterMap = new Map();
                const timerMap = new Map();
                const healthchecksSeries = [];
                const counterSeries = [];
                const gaugeSeries = [];
                const meterSeriesMap = new Map();
                const timerSeriesMap = new Map();
                results.sort(function (a, b) {
                    const aTimestamp = ko.unwrap(a[model.lucene.LOGGING_FIELD_TIMESTAMP]);
                    const bTimestamp = ko.unwrap(b[model.lucene.LOGGING_FIELD_TIMESTAMP]);
                    return (
                        aTimestamp === bTimestamp
                        ? 0
                        : (
                            aTimestamp < bTimestamp
                            ? -1
                            : 1
                        )
                    );
                }).forEach(function (result) {
                    const timestamp = dates.parseDateTime(result[model.lucene.LOGGING_FIELD_TIMESTAMP]).toMillis();
                    const checks = result.checks;
                    const metrics = result.metrics;
                    Object.keys(checks).forEach(function (key) {
                        const check = checks[key];
                        if (healthchecksSeriesMap.has(key) === false) {
                            healthchecksSeriesMap.set(key, []);
                        }
                        healthchecksSeriesMap.get(key).push([timestamp, (
                            check.healthy
                            ? 0
                            : 1
                        )]);
                    });
                    Object.keys(_.get(metrics, "counters", {})).forEach(function (key) {
                        const counter = metrics.counters[key];
                        if (counterSeriesMap.has(key) === false) {
                            counterSeriesMap.set(key, []);
                        }
                        counterSeriesMap.get(key).push([timestamp, counter.count]);
                    });
                    Object.keys(_.get(metrics, "gauges", {})).forEach(function (key) {
                        const gauge = metrics.gauges[key];
                        if (gaugeSeriesMap.has(key) === false) {
                            gaugeSeriesMap.set(key, []);
                        }
                        gaugeSeriesMap.get(key).push([timestamp, gauge.value]);
                    });
                    Object.keys(_.get(metrics, "meters", {})).forEach(function (key) {
                        const meter = metrics.meters[key];
                        if (meterMap.has(key) === false) {
                            meterMap.set(key, newMeterMap());
                        }
                        const seriesMap = meterMap.get(key);
                        Object.keys(meter).forEach(function (meterKey) {
                            if (seriesMap.has(meterKey)) {
                                seriesMap.get(meterKey).push([timestamp, meter[meterKey]]);
                            }
                        });
                    });
                    Object.keys(_.get(metrics, "timers", {})).forEach(function (key) {
                        const timer = metrics.timers[key];
                        if (timerMap.has(key) === false) {
                            timerMap.set(key, newTimerMap());
                        }
                        const seriesMap = timerMap.get(key);
                        Object.keys(timer).forEach(function (timerKey) {
                            if (seriesMap.has(timerKey)) {
                                seriesMap.get(timerKey).push([timestamp, timer[timerKey]]);
                            }
                        });
                    });
                });
                healthchecksSeriesMap.forEach((value, key) => healthchecksSeries.push({data: value, name: key, visible: true}));
                counterSeriesMap.forEach((value, key) => counterSeries.push({data: value, name: key}));
                gaugeSeriesMap.forEach((value, key) => gaugeSeries.push({data: value, name: key}));
                meterMap.forEach(function (seriesMap, key) {
                    const series = [];
                    seriesMap.forEach(function (value, skey) {
                        const visible = "mean_rate" === skey;
                        series.push({
                            data: value,
                            name: skey,
                            visible
                        });
                    });
                    meterSeriesMap.set(key, series);
                });
                timerMap.forEach(function (seriesMap, key) {
                    const series = [];
                    seriesMap.forEach(function (value, skey) {
                        const visible = "mean" === skey;
                        series.push({
                            data: value,
                            name: skey,
                            visible
                        });
                    });
                    timerSeriesMap.set(key, series);
                });
                return {
                    counterSeries,
                    gaugeSeries,
                    healthchecksSeries,
                    meterSeriesMap,
                    timerSeriesMap
                };
            };

            vm.showSeries = function (idMatcher) {
                Highcharts.charts.forEach(function (chart) {
                    if (chart) {
                        if (idMatcher(chart.options.id)) {
                            chart.series.forEach(function (serie) {
                                serie.setVisible(true, false);
                            });
                            chart.redraw();
                        }
                    }
                });
            };

            vm.hideSeries = function (idMatcher) {
                Highcharts.charts.forEach(function (chart) {
                    if (chart) {
                        if (idMatcher(chart.options.id)) {
                            chart.series.forEach(function (serie) {
                                serie.setVisible(false, false);
                            });
                            chart.redraw();
                        }
                    }
                });
            };

            vm.showHealthchecks = () => vm.showSeries((id) => "healthchecksChart" === id);
            vm.hideHealthchecks = () => vm.hideSeries((id) => "healthchecksChart" === id);
            vm.showCounters = () => vm.showSeries((id) => "metricCounterChart" === id);
            vm.hideCounters = () => vm.hideSeries((id) => "metricCounterChart" === id);
            vm.showGauges = () => vm.showSeries((id) => "metricGaugeChart" === id);
            vm.hideGauges = () => vm.hideSeries((id) => "metricGaugeChart" === id);
            vm.showMeters = () => vm.showSeries((id) => vm.meterChartNames().includes(id));
            vm.hideMeters = () => vm.hideSeries((id) => vm.meterChartNames().includes(id));
            vm.showTimers = () => vm.showSeries((id) => vm.timerChartNames().includes(id));
            vm.hideTimers = () => vm.hideSeries((id) => vm.timerChartNames().includes(id));

            vm.pushHealthchecks = function (results) {
                if (_.isEmpty(results)) {
                    return;
                }
                vm.healthchecks.removeAll();
                const healthchecks = results[0].checks;
                Object.keys(healthchecks).forEach(function (id) {
                    const check = healthchecks[id];
                    check.id = id;
                    check.message = check.message || "-";
                    vm.healthchecks.push(check);
                });
            };

            vm.scheduleLoadMetricsLiveFeed = function () {
                metricsLiveInterval = setTimeout(vm.loadMetricsLiveFeed, 1000 * 15);
            };

            vm.loadMetricsLiveFeed = function () {
                if (vm.isMetricsLive() === false || vm.isLoadingMetrics()) {
                    vm.scheduleLoadMetricsLiveFeed();
                    return;
                }
                LOG.debug("loadMetricsLiveFeed");
                vm.isLoadingMetrics(true);
                Highcharts.charts.forEach((chart) => chart?.showLoading());
                return vm.viewmodel.client.loggingMetricSearch(protocol.loggingMetricSearchRequest({
                    filter: (
                        _.isEmpty(vm.metricsFilter()) === false
                        ? vm.metricsFilter().split(",")
                        : undefined
                    ),
                    limit: 1,
                    sampling: 100
                })).then(function (searchReply) {
                    const metricTimestamp = dates.parseDateTime(searchReply.metrics[0].timestamp);
                    if (vm.metricsLiveLastTimestamp && vm.metricsLiveLastTimestamp.isSame(metricTimestamp)) {
                        return;
                    }
                    vm.metricsLiveLastTimestamp = metricTimestamp;
                    const metricsChartSeries = vm.forgeMetricsChartSeries(searchReply.metrics);
                    vm.pushHealthchecks(searchReply.metrics);
                    Highcharts.charts.forEach(function (chart) {
                        let mapSeries;
                        if (chart) {
                            const id = chart.options.id;
                            if ("healthchecksChart" === id) {
                                chart.series.forEach((serie, i) => serie.addPoint(metricsChartSeries.healthchecksSeries[i].data[0], false));
                                chart.redraw();
                            }
                            if ("metricCounterChart" === id) {
                                chart.series.forEach((serie, i) => serie.addPoint(metricsChartSeries.counterSeries[i].data[0], false));
                                chart.redraw();
                            }
                            if ("metricGaugeChart" === id) {
                                chart.series.forEach((serie, i) => serie.addPoint(metricsChartSeries.gaugeSeries[i].data[0], false));
                                chart.redraw();
                            }
                            if (metricsChartSeries.meterSeriesMap.has(id)) {
                                mapSeries = metricsChartSeries.meterSeriesMap.get(id);
                                chart.series.forEach(function (serie, i) {
                                    serie.addPoint(mapSeries[i].data[0], false);
                                });
                                chart.redraw();
                            }
                            if (metricsChartSeries.timerSeriesMap.has(id)) {
                                mapSeries = metricsChartSeries.timerSeriesMap.get(id);
                                chart.series.forEach(function (serie, i) {
                                    serie.addPoint(mapSeries[i].data[0], false);
                                });
                                chart.redraw();
                            }
                        }
                    });
                }).then(function () {
                    vm.errorReply(undefined);
                    Highcharts.charts.forEach((chart) => chart?.hideLoading());
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(() => vm.isLoadingMetrics(false));
            };

            vm.loadMetrics = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                LOG.debug("loadMetrics");
                vm.isLoadingMetrics(true);
                if (metricsLiveInterval) {
                    clearTimeout(metricsLiveInterval);
                    metricsLiveInterval = undefined;
                }
                Highcharts.charts.forEach((chart) => chart?.showLoading());
                vm.meterChartNames.removeAll();
                vm.timerChartNames.removeAll();
                const from = dates.parseDateTime(vm.metricsFrom());
                const to = dates.parseDateTime(vm.metricsTo());
                const query = (
                    (from && to)
                    ? `${model.lucene.LOGGING_FIELD_TIMESTAMP}:${model.lucene.newDateTimeRangeQuery(from, to)}`
                    : null
                );
                return vm.viewmodel.client.loggingMetricSearch(protocol.loggingMetricSearchRequest({
                    filter: (
                        _.isEmpty(vm.metricsFilter()) === false
                        ? vm.metricsFilter().split(",")
                        : undefined
                    ),
                    limit: vm.metricsLimit(),
                    query,
                    sampling: vm.metricsSampling()
                })).then(function (searchReply) {
                    const results = searchReply.metrics;
                    const metricsChartSeries = vm.forgeMetricsChartSeries(results);
                    vm.pushHealthchecks(results);
                    Highcharts.chart(
                        "healthchecksChart",
                        newChartOptions("healthchecksChart", undefined, metricsChartSeries.healthchecksSeries)
                    );
                    Highcharts.chart(
                        "metricCounterChart",
                        newChartOptions("metricCounterChart", undefined, metricsChartSeries.counterSeries)
                    );
                    Highcharts.chart(
                        "metricGaugeChart",
                        newChartOptions("metricGaugeChart", undefined, metricsChartSeries.gaugeSeries)
                    );
                    metricsChartSeries.meterSeriesMap.forEach(function (ignore, key) {
                        vm.meterChartNames.push(key);
                    });
                    metricsChartSeries.timerSeriesMap.forEach(function (ignore, key) {
                        vm.timerChartNames.push(key);
                    });
                    ko.tasks.runEarly();
                    metricsChartSeries.meterSeriesMap.forEach(function (meterHsSeries, key) {
                        Highcharts.chart(key, newChartOptions(key, key, meterHsSeries));
                    });
                    metricsChartSeries.timerSeriesMap.forEach(function (timerHsSeries, key) {
                        Highcharts.chart(key, newChartOptions(key, key, timerHsSeries));
                    });
                    vm.scheduleLoadMetricsLiveFeed();
                }).then(function () {
                    vm.errorReply(undefined);
                    Highcharts.charts.forEach((chart) => chart?.hideLoading());
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    vm.isLoadingMetrics(false);
                    ui.resetForm(form);
                });
            };

            vm.isDeleteActive = ko.observable();
            vm.deleteEntries = ko.observable(false);
            vm.deleteMetrics = ko.observable(false);
            vm.deleteBefore = ko.observable();
            vm.deleteBeforeSub = dates.insertDateTimes({fromDateObservable: vm.deleteBefore});
            vm.isDeleteEntriesValid = ko.pureComputed(() => vm.deleteEntries() || vm.deleteMetrics());
            vm.isDeleting = ko.observable();

            vm.activateDelete = () => vm.isDeleteActive(true);

            vm.loggingDelete = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                LOG.debug("loggingDelete");
                vm.isDeleting(true);
                return vm.viewmodel.client.loggingDelete(protocol.loggingDeleteRequest({
                    before: vm.deleteBefore(),
                    entries: vm.deleteEntries(),
                    metrics: vm.deleteMetrics()
                })).then(function () {
                    vm.errorReply(undefined);
                    ui.resetForm(form);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(() => vm.isDeleting(false));
            };

            vm.navigateToTab = vm.viewmodel.navigateToTab.bind(undefined, vm.viewmodel.COMPONENTS.LOGGING);

            vm.onNavUpdate = function (tab) {
                const activeTab = (
                    _.isEmpty(tab)
                    ? vm.viewmodel.LOGGING_TABS.ENTRIES
                    : tab
                );
                LOG.debug(`onNavUpdate, tab=${tab}, activeTab=${activeTab}`);
                vm.activeTab(activeTab);
                return keyval.get(SEARCH_QUERY_KEY).then(function (entryQuery) {
                    if (_.isEmpty(entryQuery)) {
                        return;
                    }
                    const queryParts = entryQuery.split("|");
                    vm.entryQuery(queryParts[0]);
                    if (1 < queryParts.length) {
                        vm.entryFrom(queryParts[1]);
                        vm.entryTo(queryParts[2]);
                    }
                });
            };

            nav.register({
                id: vm.viewmodel.COMPONENTS.LOGGING,
                onUpdate: vm.onNavUpdate
            });

            vm.dispose = function () {
                LOG.debug("dispose");
                vm.entryFromSub.dispose();
                vm.metricsFromSub.dispose();
                vm.deleteBeforeSub.dispose();
            };

            return Object.freeze(vm);
        }
    }
});