/*! 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);
}
}
});