import { describeInterval } from '@grafana/data/src/datetime/rangeutil'; import { TimeOptions } from '../types/time'; /** * ⚠️ * Some of these functions might be confusing, but there is a significant difference between "Golang duration", * supported by the time.ParseDuration() function and "prometheus duration" which is similar but does not support anything * smaller than seconds and adds the following supported units: "d, w, y" */ export function parseInterval(value: string): [number, string] { const match = value.match(/(\d+)(\w+)/); if (match) { return [Number(match[1]), match[2]]; } throw new Error(`Invalid interval description: ${value}`); } export function intervalToSeconds(interval: string): number { const { sec, count } = describeInterval(interval); return sec * count; } export const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({ label: key[0].toUpperCase() + key.slice(1), value: value, })); export function isValidPrometheusDuration(duration: string): boolean { try { parsePrometheusDuration(duration); return true; } catch (err) { return false; } } const PROMETHEUS_SUFFIX_MULTIPLIER: Record = { ms: 1, s: 1000, m: 60 * 1000, h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000, w: 7 * 24 * 60 * 60 * 1000, y: 365 * 24 * 60 * 60 * 1000, }; const DURATION_REGEXP = new RegExp(/^(?:(?\d+)(?ms|s|m|h|d|w|y))|0$/); const INVALID_FORMAT = new Error( `Must be of format "(number)(unit)", for example "1m", or just "0". Available units: ${Object.values( TimeOptions ).join(', ')}` ); /** * According to https://prometheus.io/docs/alerting/latest/configuration/#configuration-file * see * * @returns Duration in milliseconds */ export function parsePrometheusDuration(duration: string): number { let input = duration; const parts: Array<[number, string]> = []; function matchDuration(part: string) { const match = DURATION_REGEXP.exec(part); const hasValueAndType = match?.groups?.value && match?.groups?.type; if (!match || !hasValueAndType) { throw INVALID_FORMAT; } if (match && match.groups?.value && match.groups?.type) { input = input.replace(match[0], ''); parts.push([Number(match.groups.value), match.groups.type]); } if (input) { matchDuration(input); } } matchDuration(duration); if (!parts.length) { throw INVALID_FORMAT; } const totalDuration = parts.reduce((acc, [value, type]) => { const duration = value * PROMETHEUS_SUFFIX_MULTIPLIER[type]; return acc + duration; }, 0); return totalDuration; } /** * Formats the given duration in milliseconds into a human-readable string representation. * * @param milliseconds - The duration in milliseconds. * @returns The formatted duration string. */ export function formatPrometheusDuration(milliseconds: number): string { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const weeks = Math.floor(days / 7); const years = Math.floor(days / 365); // we'll make an exception here for 0, 0ms seems a bit weird if (milliseconds === 0) { return '0s'; } const timeUnits: Array<[number, string]> = [ [years, 'y'], [weeks % 52, 'w'], [(days % 365) - 7 * (weeks % 52), 'd'], [hours % 24, 'h'], [minutes % 60, 'm'], [seconds % 60, 's'], [milliseconds % 1000, 'ms'], ]; return ( timeUnits // remove all 0 values .filter(([time]) => time > 0) // join time and unit .map(([time, unit]) => time + unit) .join('') ); } export const safeParsePrometheusDuration = (duration: string): number => { try { return parsePrometheusDuration(duration); } catch (e) { return 0; } }; export const isNullDate = (date: string) => { return date.includes('0001-01-01T00'); }; // Format given time span in MS to the largest single unit duration string up to hours. export function msToSingleUnitDuration(rangeMs: number): string { if (rangeMs % (1000 * 60 * 60) === 0) { return rangeMs / (1000 * 60 * 60) + 'h'; } if (rangeMs % (1000 * 60) === 0) { return rangeMs / (1000 * 60) + 'm'; } if (rangeMs % 1000 === 0) { return rangeMs / 1000 + 's'; } return rangeMs.toFixed() + 'ms'; }