相對時間表達式 —— 解決相對時間序列化的問題

平時開發監控系統時免不了與時序數據庫的查詢打交道,在查時序數據庫時 時間範圍 是必不可少的條件,因此在查詢的UI展現上一般會將時間範圍做爲一個獨立的組件來讓用戶交互。javascript

時間範圍一般會展現爲兩種形式:相對時間和絕對時間。對於監控系統來講,平常觀察指標、創建看板基本都是使用相對時間,由於使用絕對時間的話一是不能及時更新,二是容易引起慢查詢。而絕對時間的使用場景通常是定位具體問題。前端

在咱們的監控前端裏主要使用相對時間的地方有兩個,一是adhoc查詢,另外一個是看板。在這兩處需求裏都須要對相對時間序列化,前者用來分享查詢連接,後者用來保存看板配置。下面就談談如何序列化相對時間。java

使用key來映射

這是一開始監控裏使用的方式,就是經過一些預約義的key(yesterday, today, thisweek等)來保存相對時間範圍,前端在展現時須要額外寫死的 Label MapDuration Mapgit

const LabelMap = {
  yesterday: '昨天',
  today: '今天',
  thisweek: '這周',
  // and so on..
};

const DurationMap = {
  yesterday: () => [moment().subtract(1, 'day').startOf('day'), moment().subtract(1, 'day').endOf('day')],
  today: () => [moment().startOf('day'), moment().endOf('day')],
  thisweek: () => [moment().startOf('week'), moment().endOf('week')],
  // and so on..
}
複製代碼

這種方式很簡單但不靈活,若是須要一個新的時間段就必須改這兩個Map才行。並且若是用戶有一些特殊的相對時間的話,這種方案就行不通了。github

使用結構化數據

爲了靈活性考慮,咱們可使用對象來保存相對時間,這裏咱們須要先理解相對時間由什麼組成。typescript

相對時間的抽象

在項目裏咱們通常用的時間段都是由一個開始點和一個結束點構成,其中一個相對時間點是由一連串計算產生的,這裏的計算咱們能夠分爲兩類:偏移和區間首尾。對應的moment方法爲數據庫

// 偏移
moment().add(1, 'hour');
moment().subtract(1, 'day');

// 區間首尾
moment().startOf('hour');
moment().endOf('day');
複製代碼

實現

對應的數據結構以下express

type Unit = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y';

interface Offset {
  type: 'Offset';
  // 用來表示 add 或者 subtract,通常實際使用都是 subtract 因此能夠省略
  // op: '+' | '-';
  number: number;
  unit: Unit;
}

interface Period {
  type: 'Period';
  // 用來表示 startOf 或 endOf,實際使用時可使用開始和結束點來區分,因此也能夠省略
  // op: 'start' | 'end';
  unit: Unit;
}

type Calc = Offset | Period;

interface TimeRange {
  start: Array<Calc>;
  end: Array<Calc>;
}
複製代碼

另外只要根據這個數據結構實現一個展現Label的函數和一個計算Duration的函數就好了。後端

結構化數據提供了很好的靈活性但暴露了幾個缺點:瀏覽器

  1. 展現Label的函數很差寫,尤爲是對於兩步以上的計算就得寫不少特殊判斷,好比 上週 咱們的數據長這樣(對象寫起來太長,用moment表示一下)[moment().sutract(1, 'w').startOf('w'), moment().sutract(1, 'w').endOf('w')],反過來將該對象格式化就得寫不少判斷代碼才行。
  2. 爲了方便使用,確定是須要快速篩選,不管這個列表放在前端仍是後端都須要寫一大堆代碼(快速篩選以下)
    Quick ranges
  3. 對象不太方便放到query裏,好比在咱們監控看板裏有一個功能,可讓用戶在query裏帶上時間參數來覆蓋看板裏的默認配置,若是這裏是對象的話就不太方便了。

使用相對時間表達式

若是能用表達式來表示上面的結構化數據的話不就能解決以上幾條缺點了嗎?

相對時間表達式

在這點上Grafana已經提供了一個可用的雛形,我在其語法基礎上重寫了邏輯,增長了容錯性以及語法特性,獨立出來了一個庫(主頁)。這個表達式是基於上一節結構化數據實現的,可是能更簡單明瞭。好比(取自examples

  • now - 12h: 12 hours ago, same as moment().subtract(12, 'hours')
  • -1d: 1 day ago, same as moment().subtract(1, 'day')
  • now / d: the start of today, same as moment().startOf('day')
  • now \ w: the end of this week, same as moment().endOf('week')
  • now - w / w: the start of last week, same as moment().subtract(1, 'week').startOf('week')

如何解決結構化數據的缺陷

如何解決格式化問題

將表達式格式化的話特殊區間就不須要寫代碼進行判斷了,只需像第一種方式裏同樣將標準格式的表達式映射到相應的文本上就好了。好比

const LabelMap = {
  'now-d/d to now-d\\d': '昨天',
  'now-w/d to now-w\\d': '上週的同一天',
  // so on..
}

import { standardize } from 'relative-time-expression';
const start = standardize(' now - 1 d /d'); // return now-d/d
const end = standardize('-d\\d'); // return now-d\d
const label = LabelMap[`${start} to ${end}`] || `${start} to ${end}`;
expect(label).toEqual('昨天');
複製代碼

固然在處理 前x小時, 前x天 這種狀況仍是須要寫一些判斷,和上節的處理差很少,以下

// const start, end = ...

import { parse } from 'relative-time-expression';

if (end === 'now') {
  // omit error catch code
  const ast = parse(start);
  if (ast.body.length === 1 && ast.body[0].type === 'Offset') {
    // 若是start只有一項偏移,那麼就能夠格式化成 `前{number}{單位}` 了
    return `前${ast.body[0].number}${ast.body[0].unit}`;
  }
  // ...
}
複製代碼

解決剩下兩個問題

值一旦變成普通字符串的話這兩個問題也就迎刃而解了。

時區問題

區間首尾的計算是基於時區的,好比now/d, 用戶指望的一般是他所在地區一天的開始時間(固然也不排除想經過另外時區的時間查數據的狀況)。若是計算相對時間實在客戶端的話,瀏覽器其實已經幫咱們設定好了正確的時區,可是服務端就不同了,它只能拿到服務器系統所在時區的時間。

因此考慮服務端計算相對時間的需求(監控看板裏就有相似需求:經過看板組件id直接調用後端接口拿到數據),客戶端在調用這些接口時須要帶上時區信息。服務端的處理代碼以下

import parse from 'rte-moment';
import moment from 'moment-timezone';
const m = parse('now/d', { base: moment().tz(clientTimezone || 'Asia/Shanghai') });
moment().tz('Asia/Shanghai').startOf('day').isSame(m); // true
複製代碼

結語

在監控項目裏的時間組件基本參照了Grafana的時間組件,不得不說其在監控方面還有不少值得學習的地方。

另外該項目除了typescript外還用rust練手寫了一遍,rust給我印象最深的一點是整套項目構建、文檔生成、依賴管理的工具很是好用,上手就能夠專心寫代碼了。


本文轉自個人博客

相關文章
相關標籤/搜索