grafana/public/app/features/dashboard/services/TimeSrv.ts
Galen Kistler daf9f9cd19
Prometheus: Incremental querying profile data updates (#70701)
* add prometheusIncrementalQueryInstrumentation feature flag, update instrumentation data
2023-07-05 14:39:49 -05:00

408 lines
11 KiB
TypeScript

import { cloneDeep, extend, isString } from 'lodash';
import {
dateMath,
dateTime,
getDefaultTimeRange,
isDateTime,
rangeUtil,
RawTimeRange,
TimeRange,
toUtc,
IntervalValues,
} from '@grafana/data';
import { locationService } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import { config } from 'app/core/config';
import { AutoRefreshInterval, contextSrv, ContextSrv } from 'app/core/services/context_srv';
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../types/events';
import { TimeModel } from '../state/TimeModel';
import { getRefreshFromUrl } from '../utils/getRefreshFromUrl';
export class TimeSrv {
time: RawTimeRange;
refreshTimer: number | undefined;
refresh?: string | false;
oldRefresh?: string;
timeModel?: TimeModel;
timeAtLoad: RawTimeRange;
refreshMS?: number;
private autoRefreshBlocked?: boolean;
constructor(private contextSrv: ContextSrv) {
// default time
this.time = getDefaultTimeRange().raw;
this.timeAtLoad = getDefaultTimeRange().raw;
this.refreshTimeModel = this.refreshTimeModel.bind(this);
appEvents.subscribe(ZoomOutEvent, (e) => {
this.zoomOut(e.payload.scale, e.payload.updateUrl);
});
appEvents.subscribe(ShiftTimeEvent, (e) => {
this.shiftTime(e.payload.direction, e.payload.updateUrl);
});
appEvents.subscribe(AbsoluteTimeEvent, () => {
this.makeAbsoluteTime();
});
document.addEventListener('visibilitychange', () => {
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
this.autoRefreshBlocked = false;
this.refreshTimeModel();
}
});
}
init(timeModel: TimeModel) {
this.timeModel = timeModel;
this.time = timeModel.time;
this.refresh = timeModel.refresh;
this.initTimeFromUrl();
this.parseTime();
// remember time at load so we can go back to it
this.timeAtLoad = cloneDeep(this.time);
const range = rangeUtil.convertRawToRange(
this.time,
this.timeModel?.getTimezone(),
this.timeModel?.fiscalYearStartMonth
);
if (range.to.isBefore(range.from)) {
this.setTime(
{
from: range.raw.to,
to: range.raw.from,
},
false
);
}
if (this.refresh) {
this.setAutoRefresh(this.refresh);
}
}
getValidIntervals(intervals: string[]): string[] {
if (this.contextSrv.minRefreshInterval) {
return intervals.filter((str) => str !== '').filter(this.contextSrv.isAllowedInterval);
}
return intervals;
}
private parseTime() {
// when absolute time is saved in json it is turned to a string
if (isString(this.time.from) && this.time.from.indexOf('Z') >= 0) {
this.time.from = dateTime(this.time.from).utc();
}
if (isString(this.time.to) && this.time.to.indexOf('Z') >= 0) {
this.time.to = dateTime(this.time.to).utc();
}
}
private parseUrlParam(value: string) {
if (value.indexOf('now') !== -1) {
return value;
}
if (value.length === 8) {
const utcValue = toUtc(value, 'YYYYMMDD');
if (utcValue.isValid()) {
return utcValue;
}
} else if (value.length === 15) {
const utcValue = toUtc(value, 'YYYYMMDDTHHmmss');
if (utcValue.isValid()) {
return utcValue;
}
}
if (!isNaN(Number(value))) {
const epoch = parseInt(value, 10);
return toUtc(epoch);
}
return null;
}
private getTimeWindow(time: string, timeWindow: string) {
const valueTime = parseInt(time, 10);
let timeWindowMs;
if (timeWindow.match(/^\d+$/) && parseInt(timeWindow, 10)) {
// when time window specified in ms
timeWindowMs = parseInt(timeWindow, 10);
} else {
timeWindowMs = rangeUtil.intervalToMs(timeWindow);
}
return {
from: toUtc(valueTime - timeWindowMs / 2),
to: toUtc(valueTime + timeWindowMs / 2),
};
}
private initTimeFromUrl() {
if (config.isPublicDashboardView && this.timeModel?.timepicker?.hidden) {
return;
}
const params = locationService.getSearch();
if (params.get('time') && params.get('time.window')) {
this.time = this.getTimeWindow(params.get('time')!, params.get('time.window')!);
}
if (params.get('from')) {
this.time.from = this.parseUrlParam(params.get('from')!) || this.time.from;
}
if (params.get('to')) {
this.time.to = this.parseUrlParam(params.get('to')!) || this.time.to;
}
// if absolute ignore refresh option saved to timeModel
if (params.get('to') && params.get('to')!.indexOf('now') === -1) {
this.refresh = false;
if (this.timeModel) {
this.timeModel.refresh = false;
}
}
// but if refresh explicitly set then use that
this.refresh = getRefreshFromUrl({
urlRefresh: params.get('refresh'),
currentRefresh: this.refresh,
refreshIntervals: Array.isArray(this.timeModel?.timepicker?.refresh_intervals)
? this.timeModel?.timepicker?.refresh_intervals
: undefined,
isAllowedIntervalFn: this.contextSrv.isAllowedInterval,
minRefreshInterval: config.minRefreshInterval,
});
}
updateTimeRangeFromUrl() {
const params = locationService.getSearch();
if (params.get('left')) {
return; // explore handles this;
}
const urlRange = this.timeRangeForUrl();
const from = params.get('from');
const to = params.get('to');
// check if url has time range
if (from && to) {
// is it different from what our current time range?
if (from !== urlRange.from || to !== urlRange.to) {
// issue update
this.initTimeFromUrl();
this.setTime(this.time, false);
}
} else if (this.timeHasChangedSinceLoad()) {
this.setTime(this.timeAtLoad, true);
}
}
private timeHasChangedSinceLoad() {
return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to);
}
setAutoRefresh(interval: string | false) {
if (this.timeModel) {
this.timeModel.refresh = interval;
}
this.stopAutoRefresh();
const currentUrlState = locationService.getSearchObject();
if (!interval) {
// Clear URL state
if (currentUrlState.refresh) {
locationService.partial({ refresh: null }, true);
}
return;
}
let refresh = interval;
let intervalMs = 60 * 1000;
if (interval === AutoRefreshInterval) {
intervalMs = this.getAutoRefreshInteval().intervalMs;
} else {
refresh = this.contextSrv.getValidInterval(interval as string);
intervalMs = rangeUtil.intervalToMs(refresh);
}
this.refreshMS = intervalMs;
this.refreshTimer = window.setTimeout(() => {
this.startNextRefreshTimer(intervalMs);
this.refreshTimeModel();
}, intervalMs);
if (currentUrlState.refresh !== refresh) {
locationService.partial({ refresh }, true);
}
}
getAutoRefreshInteval(): IntervalValues {
const resolution = window?.innerWidth ?? 2000;
return rangeUtil.calculateInterval(
this.timeRange(),
resolution, // the max pixels possibles
config.minRefreshInterval
);
}
refreshTimeModel() {
this.timeModel?.timeRangeUpdated(this.timeRange());
}
private startNextRefreshTimer(afterMs: number) {
this.refreshTimer = window.setTimeout(() => {
this.startNextRefreshTimer(afterMs);
if (this.contextSrv.isGrafanaVisible()) {
this.refreshTimeModel();
} else {
this.autoRefreshBlocked = true;
}
}, afterMs);
}
stopAutoRefresh() {
clearTimeout(this.refreshTimer);
this.refreshTimer = undefined;
this.refreshMS = undefined;
}
// resume auto-refresh based on old dashboard refresh property
resumeAutoRefresh() {
if (this.timeModel?.refresh) {
this.setAutoRefresh(this.timeModel.refresh);
}
}
setTime(time: RawTimeRange, updateUrl = true) {
extend(this.time, time);
// disable refresh if zoom in or zoom out
if (isDateTime(time.to)) {
this.oldRefresh = this.timeModel?.refresh || this.oldRefresh;
this.setAutoRefresh(false);
} else if (this.oldRefresh && this.oldRefresh !== this.timeModel?.refresh) {
this.setAutoRefresh(this.oldRefresh);
this.oldRefresh = undefined;
}
if (updateUrl === true) {
const urlRange = this.timeRangeForUrl();
const urlParams = locationService.getSearchObject();
if (urlParams.from === urlRange.from.toString() && urlParams.to === urlRange.to.toString()) {
return;
}
urlParams.from = urlRange.from.toString();
urlParams.to = urlRange.to.toString();
locationService.partial(urlParams);
}
// Check if the auto refresh interval has changed
if (this.timeModel?.refresh === AutoRefreshInterval) {
const v = this.getAutoRefreshInteval().intervalMs;
if (v !== this.refreshMS) {
this.setAutoRefresh(AutoRefreshInterval);
}
}
this.refreshTimeModel();
}
timeRangeForUrl = () => {
const range = this.timeRange().raw;
if (isDateTime(range.from)) {
range.from = range.from.valueOf().toString();
}
if (isDateTime(range.to)) {
range.to = range.to.valueOf().toString();
}
return range;
};
timeRange(): TimeRange {
return getTimeRange(this.time, this.timeModel);
}
zoomOut(factor: number, updateUrl = true) {
const range = this.timeRange();
const { from, to } = getZoomedTimeRange(range, factor);
this.setTime({ from: toUtc(from), to: toUtc(to) }, updateUrl);
}
shiftTime(direction: ShiftTimeEventDirection, updateUrl = true) {
const range = this.timeRange();
const { from, to } = getShiftedTimeRange(direction, range);
this.setTime(
{
from: toUtc(from),
to: toUtc(to),
},
updateUrl
);
}
makeAbsoluteTime() {
const params = locationService.getSearch();
if (params.get('left')) {
return; // explore handles this;
}
const { from, to } = this.timeRange();
this.setTime({ from, to }, true);
}
// isRefreshOutsideThreshold function calculates the difference between last refresh and now
// if the difference is outside 5% of the current set time range then the function will return true
// if the difference is within 5% of the current set time range then the function will return false
// if the current time range is absolute (i.e. not using relative strings like now-5m) then the function will return false
isRefreshOutsideThreshold(lastRefresh: number, threshold = 0.05) {
const timeRange = this.timeRange();
if (dateMath.isMathString(timeRange.raw.from)) {
const totalRange = timeRange.to.diff(timeRange.from);
const msSinceLastRefresh = Date.now() - lastRefresh;
const msThreshold = totalRange * threshold;
return msSinceLastRefresh >= msThreshold;
}
return false;
}
}
let singleton: TimeSrv | undefined;
export function setTimeSrv(srv: TimeSrv) {
singleton = srv;
}
export function getTimeSrv(): TimeSrv {
if (!singleton) {
singleton = new TimeSrv(contextSrv);
}
return singleton;
}