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

栏目: 数据库 · 发布时间: 5年前

内容简介:平时开发监控系统时免不了与时序数据库的查询打交道,在查时序数据库时时间范围通常会展示为两种形式:相对时间和绝对时间。对于监控系统来说,日常观察指标、建立看板基本都是使用相对时间,因为使用绝对时间的话一是不能及时更新,二是容易引发慢查询。而绝对时间的使用场景一般是定位具体问题。在我们的监控前端里主要使用相对时间的地方有两个,一是adhoc查询,另一个是看板。在这两处需求里都需要对相对时间序列化,前者用来分享查询链接,后者用来保存看板配置。下面就谈谈如何序列化相对时间。

平时开发监控系统时免不了与时序数据库的查询打交道,在查时序数据库时 时间范围 是必不可少的条件,所以在查询的UI展示上通常会将时间范围作为一个独立的组件来让用户交互。

时间范围通常会展示为两种形式:相对时间和绝对时间。对于监控系统来说,日常观察指标、建立看板基本都是使用相对时间,因为使用绝对时间的话一是不能及时更新,二是容易引发慢查询。而绝对时间的使用场景一般是定位具体问题。

在我们的监控前端里主要使用相对时间的地方有两个,一是adhoc查询,另一个是看板。在这两处需求里都需要对相对时间序列化,前者用来分享查询链接,后者用来保存看板配置。下面就谈谈如何序列化相对时间。

使用key来映射

这是一开始监控里使用的方式,就是通过一些预定义的key( yesterday , today , thisweek 等)来保存相对时间范围,前端在展示时需要额外写死的 Label MapDuration Map

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才行。而且如果用户有一些特殊的相对时间的话,这种方案就行不通了。

使用结构化数据

为了灵活性考虑,我们可以使用对象来保存相对时间,这里我们需要先理解相对时间由什么组成。

相对时间的抽象

在项目里我们一般用的时间段都是由一个开始点和一个结束点构成,其中一个相对时间点是由一连串计算产生的,这里的计算我们可以分为两类:偏移和区间首尾。对应的moment方法为

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

// 区间首尾
moment().startOf('hour');
moment().endOf('day');
复制代码

实现

对应的数据结构如下

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. 为了方便使用,肯定是需要快速筛选,无论这个列表放在前端还是后端都需要写一大堆代码(快速筛选如下)
    相对时间表达式 —— 解决相对时间序列化的问题
  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给我印象最深的一点是整套项目构建、文档生成、依赖管理的 工具 非常好用,上手就可以专心写代码了。

本文转自我的博客


以上所述就是小编给大家介绍的《相对时间表达式 —— 解决相对时间序列化的问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Microsoft Windows程序设计

Microsoft Windows程序设计

佩措尔德 / 章立民 / 华中科技 / 2004-1 / 118.00元

Charles Petzold是全球最权威且知名的Windows程序设计专家,他将其最畅销Programming Microsoft Windows with C#一书加以改写,使之能完全适用于Visual Basic.NET的开发人员。这位畅销书的作家示范了如何使用Visual Basic.NET将Windows Forms的功能发挥到极致(Windows Forms是新一代的Windows程序......一起来看看 《Microsoft Windows程序设计》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具