相对时间表达式 —— 解决相对时间序列化的问题

平时开发监控系统时免不了与时序数据库的查询打交道,在查时序数据库时 时间范围 是必不可少的条件,因此在查询的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给我印象最深的一点是整套项目构建、文档生成、依赖管理的工具很是好用,上手就能够专心写代码了。


本文转自个人博客

相关文章
相关标签/搜索