时间解析

介绍 时间戳的解析有如下步骤:

  1. 提取时间字符串,需要用到 elect 来提取时间字符串
  2. 确定时区,如果容器的时区为空, 那么使用该时区
  3. 尝试将时间字符串解析为毫秒时间戳,该步骤需要了解时间字符串的格式和布局

时间戳的提取配置由如下结构体定义:

TimeConf struct {
   Type  string `json:"type"`
   Elect *Elect `json:"elect"`
   Format string `json:"format"`
   Layout string `json:"layout"`
   Timezone string `json:"timezone"`
}

type 定义了总体的解析类型:

  1. auto:全自动解析,如果选择 auto 模式,那么尝试自动从日志行里提取时间,它只适用于时间戳位于行首的简单日志,此时其他所有字段都不用配置
  2. processTime:以读到日志的时间作为时间戳,具有较大的不确定性, 此时其他所有字段都不用配置
  3. elect:从日志行里提取时间戳字符串, 然后解析成 毫秒级时间戳, 此时需要用到 elect、format、layout、timezone 字段

对于 type = elect 的情况

  1. elect: 用于提取时间字符串
  2. format: 描述了 时间字符串的 风格: a. unix: 秒级时间戳 b. unixMilli: 毫秒级时间戳 c. golangLayout: 它的格式由一个 Golang 风格的字符串描述, 此时需要用到 Layout 字段 d. auto: 类似 type=auto 的场景, 它可以自动猜测时间戳的格式和布局
  3. layout: 仅当 format = golangLayout 时用到, layout 为 Golang 风格的时间格式
  4. timezone: 时区, 不建议填写; 建议日志和容器的时区保持一致; 如果容器时区解析非空, 则优先使用容器时区

例子

例子1: 适用于 单行日志 和 多行日志(堆栈) 时间戳在首行开头的

{
  "type": "auto"
}

此时使用容器的时区来解析时间字符串。

例子2: 用户的每行日志是一个json字符串,时间戳在 myTime 字段里,对于 myTime 的时间格式采用字段解析。

{
   "type": "elect",
   "elect": {
      "type": "refName",
         "refName": {
         "name": "myTime"
      }
   },
   "format": "auto"
}

此时使用容器的时区来解析时间字符串。

例子3: 用户的每行日志是一个json字符串, 时间戳在 myTime 字段里,对于 myTime 的时间格式采用 "2006-01-02 15:04:05" Golang 风格的时间格式解析。

{
   "type": "elect",
   "elect": {
      "type": "refName",
      "refName": {
         "name": "myTime"
      }
   },
   "format": "golangLayout",
   "layout": "2006-01-02 15:04:05"
}

此时使用容器的时区来解析时间字符串。

例子4: 使用处理时间作为时间戳

{
  "type": "processTime"
}

自动时间解析

上文提到,HoloInsight-Agent 可以做一些自动地时间解析,其实它是内置支持了一些常见的时间格式的解析。 以下是支持的 Golang time layout:(注意这是 Golang 格式,并不代表最终时间字符串一定严格长这个样子)

  1. Mon Jan _2 15:04:05 MST 2006
  2. Mon Jan _2 15:04:05 2006
  3. 2006 Jan/02 15:04:05
  4. 02/Jan/2006 15:04:05
  5. Jan 02 2006 15:04:05
  6. 01/02/2006 15:04:05
  7. 2006-01-02 15:04:05.000 Z07:00
  8. 2006-01-02 15:04:05.000Z07:00
  9. 2006-01-02 15:04:05.000
  10. 2006-01-02 15:04:05 Z07:00
  11. 2006-01-02 15:04:05Z07:00
  12. 2006-01-02 15:04:05
  13. 2006/01/02 15:04:05.000 Z07:00
  14. 2006/01/02 15:04:05.000Z07:00
  15. 2006/01/02 15:04:05.000
  16. 2006/01/02 15:04:05 Z07:00
  17. 2006/01/02 15:04:05Z07:00
  18. 2006/01/02 15:04:05
  19. 2006-01-02T15:04:05.000 Z07:00
  20. 2006-01-02T15:04:05.000Z07:00
  21. 2006-01-02T15:04:05.000
  22. 2006-01-02T15:04:05 Z07:00
  23. 2006-01-02T15:04:05Z07:00
  24. 2006-01-02T15:04:05

解析的时候,格式最长的优先进行匹配,后面18种格式其实是3种格式的变种。

  • .000 可以匹配毫秒部分,比如 "2006-01-02 15:04:05,123" 或 "2006-01-02 15:04:05.123"
  • Z07:00 可以匹配时区部分,比如 "2006-01-02 15:04:05,123 Z" 或 "2006-01-02 15:04:05,123 +08:00",Z 表示 UTC 时区

时区解析

容器的日志所属的时区有3个来源,按如下优先级从高到低:

  1. 时间字符串里自带时区信息:比如 'Z' '+08:00'
  2. 容器环境变量 TZ
  3. 解析 /etc/localtime,按照规范,它必须是一个软链接,链接到 /usr/share/zoneinfo/Asia/Shanghai 之类的时区文件
    1. 在实践中,很多用户并没有让 /etc/localtime 成为一个软链接,而是直接将 /usr/share/zoneinfo/Asia/Shanghai 复制覆盖了 /etc/localtime,因此它是一个普通文件
    2. 在实践中,出现过 /etc/localtime 指向 /usr/share/zoneinfo/UTC,但 UTC 的文件内容竟然是东八区的内容:从文件名看起来是 UTC, 但实际是 CST-8 (东八区),这意味着我们需要解析文件内容才能最终确定时区,但是 zoneinfo 的文件内容只会告诉你该地理时区包含哪几个经度时区,比如 Asia/Shanghai 文件会告诉你它包含 CST-8 (东八区),但是只从文件内容是无法知道它其实是 Asia/Shanghai 这个地理时区的。因为 Asia/Chongqing 这个时区(可能已经不在用了)也包含 CST-8,因此只根据 CST-8 不能反推出 Asia/Shanghai,虽然在实践中你确实可以这么做,大概率没问题,但面临国外时区时就懵了,因为我们很难去懂外国的时区关系,特别是那些一个国家包含多个时区的
      1. 虽然说无法从 zoneinfo 解析出类似 Asia/Shanghai 之类的地理时区名称,但它依旧可以用于正确解析出时间(因为时区名称只是一个名字,它指向一堆时区规则)。因此解析不出 Asia/Shanghai 之类的名称根本无所谓,只是看起来明确好看而已,我们能解出 CST-8 这个名称已经很大程度解决问题了
    3. 上述提到的两种错误实践其实挺常见的,因此 Agent 对此也做了支持
  4. 如果上述手段都不能解出时区,默认使用 UTC 时区

注意,时区的解析只会发生在 Agent 第一次碰到该容器时,在Agent进程的生命周期内只会执行一次。因此通过 k8s 的 postStart 钩子去修改时区这种行为是无效的,因为此时容器已经启动成功了,这种行为也不会影响已经启动的进程对时区的认知。

错误做法: 某个容器直接把 /etc/localtime 链接到 zoneinfo 目录,这完全是不符合规范的。此时默认为 UTC 时区。

推荐时区配置方式

  1. 为容器声明环境变量 TZ,比如在 k8s 的 yaml 里就很合适,或者直接在 Dockerfile 里声明 ENV TZ=Asia/Shanghai
  2. 在 Dockerfile 里将 /etc/localtime 软链接到 /usr/share/zoneinfo/Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime