toml 简介

toml

1. 简介

编程中经常需要使用配置文件,常见配置文件有用INI, XML, JSON, YAML 语言编写,它们的表达力越来越强,同时书写便捷性也在不断提升。

TOML 全称: Tom’s Obvious, Minimal Language, 对应中文为 Tom的(语义)明显、(配置)最小化的语言。

TOML 是前GitHub CEO, Tom Preston-Werner,于2013年创建的语言,其目标是成为一个小规模的易于使用和阅读的语义化配置文件格式。

TOML 被设计成可以无歧义地映射为哈希表, 而编程语言中都会有类似哈希表的数据结构,从而可以很容易的被各种语言解析。

需要注意: 此规范当前为0.4 版,仍然会发生很多变化。

2. 语法

toml 语法规范

  • TOML是大小写敏感的。
  • TOML文件需要是UTF-8 编码。
  • 空格是指制表符(0x09) 或空格 (0x20)。
  • 换行符是指LF(0x0A)或CRLF (0x0D0A)。

配置示例

3. 变量类型

  • 字符串
  • 整数
  • 浮点数
  • 布尔
  • 日期
  • 数组
  • 嵌套表
  • 内联表
  • 表数组

注释

用符号#来表示注释:

键名

键值对左边是键,右边是值。键名和值周围的空格都将被忽略,键、等号和值,一定要在同一行(有些值可以多行表示)

键可以是裸键,或者由双引号包括,不可以是单引号。

裸键 仅包含字母、数字、下划线和破折号。

引号键 遵循基本字符串的规则,可以使用更广泛的键名。除非有必要,否则建议使用裸键。

字符串

有四种表示方法:基本字符串、多行基本字符串、字面量和多行字面量。

基本字符串 双引号括起来的任意字符串,包含双引号、反斜杠和控制字符等转义字符。

多行基本字符串 三个双引号括起来的字符串,允许换行,所以可以将很长的字符串分成多行。

注意: 行尾存在\ 时,将会删除当前位置到下个非空字符或结束界定符之间的所有空格(包括换行符),该特性可以用于编写长字符串。所有的转义字符在多行基本字符串有效。

字面量字符串的特点是不允许转义,有基本字面量字符串和多行字面量字符串。

字面量字符串 是被单引号包含的字符串,跟基本字符串一样,它们一定是以单行出现:

因为不支持转义,所以字面量字符串里面没有办法写单引号。但是可以在多行字面量字符串中使用。

多行字面量字符串 被三个单引号括起来的字符串,允许换行。

紧跟起始界定符的换行符会被剪掉。界定符之间的所有其他内容都会被按照原样解释而不会被转义。

对于二进制数据,建议你使用Base64或其他适合的编码,比如ASCII或UTF-8编码。具体的处理取决于特定的应用。

整数

整数是没有小数点的数字。正数前面也可以用加号,负数需要用负号前缀。

对于大整数,可以用下划线提高可读性。每个下划线两边至少包含一个数字。

前导零是不允许的。也不允许十六进制(Hex)、八进制(octal)和二进制形式。

TOML 中不能表示诸如“无穷”和“非数字”,不能用一串数字表示的值。数字范围是64位从−9,223,372,036,854,775,808 到9,223,372,036,854,775,807。

浮点数

一个浮点数由整数部分(可能是带有加号或减号前缀的)和小数部分和(或)指数部分组成的数。如果只有小数部分和指数部分,那么小数部分必须放在指数部分前面。

小数部分是指在小数点后面的一个或多个数字。

指数部分是指E(大写或小写)后面的整数部分(可能用加号或减号为前缀)

和整数类似,你可以用下划线来提高可读性。每个下划线两边至少包含一个数字。

数据精度为64位 (double)。

布尔值

布尔值是小写的true和false。

时间日期

时间日期是RFC 3339中的时间格式。

数组

数组是由方括号包括的数据类型,元素由逗号分隔,元素的数据类型不能混用(所有的字符串均为同一类型,但是整形和浮点型是两种类型)。

数组可以写成多行,解析时会忽略空格和换行符。结束括号之前可以存在逗号。

表(也被称为哈希表或字典)是键值对集合,且键值对是无序的。表名由方括号括起,自成一行。

表的范围是在表名之下,直到下一个表或文件尾(EOF)之间都是该表的键值对。

表和数组相区分,数组里只有值。

嵌套表

使用点(.)来表示嵌套表,点前面的部分属于父表,点后面的部分属于子表。

等价于如下JSON格式:

编写时,你可以不用定义每一层父表(super-tables)。

只要父表没有被直接定义,而且没有定义特定的键,你可以继续写入。

你不能多次定义键或表。这样做是无效的。

被点分隔部分周围的空格都会被忽略,但是最好不要使用任何多余的空格

所有的表名和键一定不能为空

内联表

内联表提供一种更紧凑的语法来表示表,内联表是由大括号{}括起来的,在大括号内可以存在零个或多个逗号分隔的键值对。

内联表里的键值对跟标准表里的键值对形式一样并且允许所有的值类型,包括内联表。

内联表一般以单行出现。不允许换行符出现在大括号之间,除非是包含在值中的有效字符。
即便如此,也强烈建议不要在把内联表分成多行。如果你有这种需求,那么你应该去用标准表。

对应标准表定义:

表数组

表数组使用双中括号包裹表名,使用相同双括号名的每个表都是数组中的元素。表的顺序跟书写顺序一致。没有键值对的双括号表会被当作空表。

等价于如下JSON格式:

你也能创建内嵌的表数组。只需要对子表使用相同的双括号语法就可以。

上面的TOML对应于下面的JSON格式:

PHP toml 解析演示

安装解析库

解析示例

资料

elk 部署架构

elk 部署架构

一、概述

ELK 已经成为目前最流行的集中式日志解决方案,它主要是由Beats、Logstash、Elasticsearch、Kibana等组件组成,来共同完成实时日志的收集,存储,展示等一站式的解决方案。本文将会介绍ELK常见的架构以及相关问题解决。

Filebeat:Filebeat是一款轻量级,占用服务资源非常少的数据收集引擎,它是ELK家族的新成员,可以代替Logstash作为在应用服务器端的日志收集引擎,支持将收集到的数据输出到Kafka,Redis等队列。
Logstash:数据收集引擎,相较于Filebeat比较重量级,但它集成了大量的插件,支持丰富的数据源收集,对收集的数据可以过滤,分析,格式化日志格式。
Elasticsearch:分布式数据搜索引擎,基于Apache Lucene实现,可集群,提供数据的集中式存储,分析,以及强大的数据搜索和聚合功能。
Kibana:数据的可视化平台,通过该web平台可以实时的查看 Elasticsearch 中的相关数据,并提供了丰富的图表统计功能。

二、ELK常见部署架构

2.1、Logstash作为日志收集器

这种架构是比较原始的部署架构,在各应用服务器端分别部署一个Logstash组件,作为日志收集器,然后将Logstash收集到的数据过滤、分析、格式化处理后发送至Elasticsearch存储,最后使用Kibana进行可视化展示,这种架构不足的是:Logstash比较耗服务器资源,所以会增加应用服务器端的负载压力。

2.2、Filebeat作为日志收集器

该架构与第一种架构唯一不同的是:应用端日志收集器换成了Filebeat,Filebeat轻量,占用服务器资源少,所以使用Filebeat作为应用服务器端的日志收集器,一般Filebeat会配合Logstash一起使用,这种部署方式也是目前最常用的架构。

2.3、引入缓存队列的部署架构

该架构在第二种架构的基础上引入了Redis缓存队列(还可以是其他消息队列),将Filebeat收集到的数据发送至Redis,然后在通过Logstasth读取Redis中的数据,这种架构主要是解决大数据量下的日志收集方案,使用缓存队列主要是解决数据安全与均衡Logstash与Elasticsearch负载压力。

2.4、以上三种架构的总结

第一种部署架构由于资源占用问题,现已很少使用,目前使用最多的是第二种部署架构,至于第三种部署架构个人觉得没有必要引入消息队列,除非有其他需求,因为在数据量较大的情况下,Filebeat 使用压力敏感协议向 Logstash 或 Elasticsearch 发送数据。如果 Logstash 正在繁忙地处理数据,它会告知 Filebeat 减慢读取速度。拥塞解决后,Filebeat 将恢复初始速度并继续发送数据。

三、问题及解决方案

问题:如何实现日志的多行合并功能?

系统应用中的日志一般都是以特定格式进行打印的,属于同一条日志的数据可能分多行进行打印,那么在使用ELK收集日志的时候就需要将属于同一条日志的多行数据进行合并。

解决方案:使用Filebeat或Logstash中的multiline多行合并插件来实现

在使用multiline多行合并插件的时候需要注意,不同的ELK部署架构可能multiline的使用方式也不同,如果是本文的第一种部署架构,那么multiline需要在Logstash中配置使用,如果是第二种部署架构,那么multiline需要在Filebeat中配置使用,无需再在Logstash中配置multiline。

1、multiline在Filebeat中的配置方式:

filebeat.prospectors:
    -
       paths:
          - /home/project/elk/logs/test.log
       input_type: log 
       multiline:
            pattern: '^\['
            negate: true
            match: after
output:
   logstash:
      hosts: ["localhost:5044"]
  • pattern:正则表达式
  • negate:默认为false,表示匹配pattern的行合并到上一行;true表示不匹配pattern的行合并到上一行
  • match:after表示合并到上一行的末尾,before表示合并到上一行的行首

如:

pattern: '\['
negate: true
match: after

该配置表示将不匹配pattern模式的行合并到上一行的末尾

2、multiline在Logstash中的配置方式

input {
  beats {
    port => 5044
  }
}

filter {
  multiline {
    pattern => "%{LOGLEVEL}\s*\]"
    negate => true
    what => "previous"
  }
}

output {
  elasticsearch {
    hosts => "localhost:9200"
  }
}

(1)Logstash中配置的what属性值为previous,相当于Filebeat中的after,Logstash中配置的what属性值为next,相当于Filebeat中的before。
(2)pattern => “%{LOGLEVEL}\s*]” 中的LOGLEVEL是Logstash预制的正则匹配模式,预制的还有好多常用的正则匹配模式,详细请看:https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns

问题:如何将Kibana中显示日志的时间字段替换为日志信息中的时间?

默认情况下,我们在Kibana中查看的时间字段与日志信息中的时间不一致,因为默认的时间字段值是日志收集时的当前时间,所以需要将该字段的时间替换为日志信息中的时间。

解决方案:使用grok分词插件与date时间格式化插件来实现

在Logstash的配置文件的过滤器中配置grok分词插件与date时间格式化插件,如:

input {
  beats {
    port => 5044
  }
}

filter {
  multiline {
    pattern => "%{LOGLEVEL}\s*\]\[%{YEAR}%{MONTHNUM}%{MONTHDAY}\s+%{TIME}\]"
    negate => true
    what => "previous"
  }

  grok {
    match => [ "message" , "(?<customer_time>%{YEAR}%{MONTHNUM}%{MONTHDAY}\s+%{TIME})" ]
  }

  date {
        match => ["customer_time", "yyyyMMdd HH:mm:ss,SSS"] //格式化时间
        target => "@timestamp" //替换默认的时间字段
  }
}

output {
  elasticsearch {
    hosts => "localhost:9200"
  }
}

如要匹配的日志格式为:“[DEBUG][20170811 10:07:31,359][DefaultBeanDefinitionDocumentReader:106] Loading bean definitions”,解析出该日志的时间字段的方式有:

① 通过引入写好的表达式文件,如表达式文件为customer_patterns,内容为:
CUSTOMER_TIME %{YEAR}%{MONTHNUM}%{MONTHDAY}\s+%{TIME}
注:内容格式为:[自定义表达式名称] [正则表达式]
然后logstash中就可以这样引用:

filter {
  grok {
      patterns_dir => ["./customer-patterms/mypatterns"] //引用表达式文件路径
      match => [ "message" , "%{CUSTOMER_TIME:customer_time}" ] //使用自定义的grok表达式
  }
}

② 以配置项的方式,规则为:(?<自定义表达式名称>正则匹配规则),如:

filter {
  grok {
    match => [ "message" , "(?<customer_time>%{YEAR}%{MONTHNUM}%{MONTHDAY}\s+%{TIME})" ]
  }
}

问题:如何在Kibana中通过选择不同的系统日志模块来查看数据

一般在Kibana中显示的日志数据混合了来自不同系统模块的数据,那么如何来选择或者过滤只查看指定的系统模块的日志数据?

解决方案:新增标识不同系统模块的字段或根据不同系统模块建ES索引

1、新增标识不同系统模块的字段,然后在Kibana中可以根据该字段来过滤查询不同模块的数据
这里以第二种部署架构讲解,在Filebeat中的配置内容为:

filebeat.prospectors:
    -
       paths:
          - /home/project/elk/logs/account.log
       input_type: log 
       multiline:
            pattern: '^\['
            negate: true
            match: after
       fields: //新增log_from字段
         log_from: account

    -
       paths:
          - /home/project/elk/logs/customer.log
       input_type: log 
       multiline:
            pattern: '^\['
            negate: true
            match: after
       fields:
         log_from: customer
output:
   logstash:
      hosts: ["localhost:5044"]

通过新增:log_from字段来标识不同的系统模块日志

2、根据不同的系统模块配置对应的ES索引,然后在Kibana中创建对应的索引模式匹配,即可在页面通过索引模式下拉框选择不同的系统模块数据。
这里以第二种部署架构讲解,分为两步:

① 在Filebeat中的配置内容为:

filebeat.prospectors:
    -
       paths:
          - /home/project/elk/logs/account.log
       input_type: log 
       multiline:
            pattern: '^\['
            negate: true
            match: after
       document_type: account

    -
       paths:
          - /home/project/elk/logs/customer.log
       input_type: log 
       multiline:
            pattern: '^\['
            negate: true
            match: after
       document_type: customer
output:
   logstash:
      hosts: ["localhost:5044"]

通过document_type来标识不同系统模块

② 修改Logstash中output的配置内容为:

output {
  elasticsearch {
    hosts => "localhost:9200"
    index => "%{type}"
  }
}

在output中增加index属性,%{type}表示按不同的document_type值建ES索引

四、总结

本文主要介绍了ELK实时日志分析的三种部署架构,以及不同架构所能解决的问题,这三种架构中第二种部署方式是时下最流行也是最常用的部署方式,最后介绍了ELK作在日志分析中的一些问题与解决方案,说在最后,ELK不仅仅可以用来作为分布式日志数据集中式查询和管理,还可以用来作为项目应用以及服务器资源监控等场景,更多内容请看官网。

参考资料

url 与转义

url 与转义

URI

URI,全称是 Uniform Resource Identifiers,即统一资源标识符,用于在互联网上标识一个资源,比如 https://www.upyun.com/products/cdn 这个 URI,指向的是一张漂亮的,描述又拍云 CDN 产品特性的网页。

URI 的组成

完整的 URI,由四个主要的部分构成:

<scheme>://<authority><path>?<query>

scheme 表示协议,比如 http,ftp 等等,详细介绍可以参考 rfc2396#section-3.1。

authority,用 :// 来和 scheme 区分。从字面意思看就是“认证”,“鉴权”的意思,引用 rfc2396#secion-3.2 的一句话:

这个“认证”部分,由一个基于 Internet 的服务器定义或者由命名机关注册登记(和具体的协议有关)。

而常见的 authority 则是:“由基于 Internet 的服务器定义”,其格式如下:

<userinfo>@<host>:<port>

userinfo 这个域用于填写一些用户相关的信息,比如可能会填写 “user:password”,当然这是不被建议的,抛开这个不讲,后面的 : 则是被熟知的服务器地址了,host 可以是域名,也可以是对应的 IP 地址,port 表示端口,这是一个可选项,如果不填写,会使用默认端口(和协议相关,比如 http 是 80)。

path,在 scheme 和 authority 确定下来的情况下标识资源,path 由几个段组成,每个段用 / 来分隔,path 不等同于文件系统定义的路径。

query,查询串(或者说参数串),用 ? 和 path 区分开来,其具体的含义由这个资源来定义。

保留字符

从上面的描述里看,URI 的这 4 个组件,由特定的分隔符来分离,这些分隔符有着它们的特殊含义,如果在某个部分里出现这些分隔符,比如在 path 是 /a/b?c.html,那么 c.html 会被当做是 query,这样就破坏了原本的含义,因此 URI 引入保留字符集,这些字符用于特殊目的,如果它们被用于描述资源(不是作为分隔符出现),那么必须对它们转义。
那么什么情况下需要对一个字符转义呢,引用 rfc2395#section-2.2 的一句话:

In general, a character is reserved if the semantics of the URI changes if the character is replaced with its escaped US-ASCII encoding.

即如果转义前后这个字符会影响到整个 URI 的意义,则它必须被转义。

由于 URI 由多个部分构成,一个字符不转义,可能会对其中一个部分会造成影响,但对另一个部分没有影响,所以“保留字符集”是由具体的 URI 组成部分来规定。

  • 对 path 部分而言,保留字符集是(参考自 rfc2396):

reserved = “/” | “?” | “;” | “=”

  • 对 query 部分而言,保留字符集是(参考自 rfc2396):

reserved = “;” | “/” | “?” | “:” | “@” | “&” | “=” | “+” | “,” | “$”

字符的转义规则如下:

escaped = "%" hex hex
hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
"a" | "b" | "c" | "d" | "e" | "f"

比如 , 转义后为 %2C。

特殊字符

有一类不被允许用在 URI 里的特殊字符,它们被称为控制字符,即 ASCII 范围在0-31 之间的字符,以及 ASCII 码为 127 的这个字符。比如 \t,\a 这些(不包括空格),因为这些字符不可打印而且可能会消失(在某些场景下)。
另外一类则是扩展 ASCII 码,即范围 128-255 的那些字符,它们不属于 “US-ASCII coded character set”,因此这些字符如果出现在 URI 中,需要被转义。
URLs are written only with the graphic printable characters of the US-ASCII coded character set. The octets 80-FF hexadecimal are not used in US-ASCII, and the octets 00-1F and 7F hexadecimal represent control characters; these must be encoded.

不安全字符

Characters can be unsafe for a number of reasons. The space character is unsafe because significant spaces may disappear and insignificant spaces may be introduced when URLs are transcribed or typeset or subjected to the treatment of word-processing programs. The characters “<” and “>” are unsafe because they are used as the delimiters around URLs in free text; the quote mark (“””) is used to delimit URLs in some systems. The character “#” is unsafe and should always be encoded because it is used in World Wide Web and in other systems to delimit a URL from a fragment/anchor identifier that might follow it. The character “%” is unsafe because it is used for encodings of other characters. Other characters are unsafe because gateways and other transport agents are known to sometimes modify such characters. These characters are “{“, “}”, “|”, “”, “^”, “~”, “[”, “]”, and “`”.
这段话引用自 rfc1738 2.2 节。因为种种的原因,存在一类字符,它们是 “unsafe” 的,不加处理地存在在 URI 里,会破坏 URI 的语义完整性,对于这类字符,如果要出现在 URI 里,那么也得被转义。

nginx 的 URI 转义机制

nginx (以现在最新的 1.13.6 版本为准)提供了一个名为 ngx_escape_uri 的函数,函数原型如下:

uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,ngx_uint_t type);

第三个参数,type,可以接受这些值:

#define NGX_ESCAPE_URI 0
#define NGX_ESCAPE_ARGS 1
#define NGX_ESCAPE_URI_COMPONENT 2
#define NGX_ESCAPE_HTML 3
#define NGX_ESCAPE_REFRESH 4
#define NGX_ESCAPE_MEMCACHED 5
#define NGX_ESCAPE_MAIL_AUTH 6

我们只关心其中的 NGX_ESCAPE_URI ,NGX_ESCAPE_ARGS ,NGX_ESCAPE_URI_COMPONENT 三个,根据 nginx 官方提供的 nginx 各模块和核心 API 介绍,这三个宏的含义如下:

NGX_ESCAPE_URI: Escape a standard URI
NGX_ESCAPE_ARGS: Escape query arguments
NGX_ESCAPE_URI_COMPONENT: Escape the URI after the domain

对应地,ngx_escape_uri 这个函数,内置了几个相关的 bitmap,区别在于各自的转义字符集,具体可以查阅 nginx 的源码(ngx_string.c#1441)。
其中针对整个 URI 的转义处理,ngx_escapeuri 会把 ” “, “#”, “%”, “?” 以及 %00-%1F 和 %7F-%FF 的字符转义;针对 query 的转义,会把 ” “, “#”, “%”, “&”, “+”, “?” 以及 %00-%1F 和 %7F-%FF 的字符转义;针对 path + query(the URI after the domain)的转义,会把除英文字母,数字,以及 “-“, “.”, ““, “~” 这些以外的字符全部转义。
可以看到,NGX_ESCAPE_URI 和 NGX_ESCAPE_ARGS 没有处理不安全字符,前者站在处理整个的 URI 的角度上编码,因此 ‘/’, ‘&’ 等分割符字符没有编码(但是对 ‘?’ 却编码,这点的原因目前未知),后者站在处理 query 的角度上编码,所以对于会破坏 query 串语义的字符,都进行了编码;而 NGX_ESCAPE_URI_COMPONENT ,处理角度不是整个 URI,而是 domain 之后的 URI 组件,它兼顾 path 和 query 的保留字符集,更加严格,遵守了 rfc3986#section-2.2 的规范。
这里顺便提一下 ngx_proxy 模块对应的 URI 转义处理,在构造向上游发送的请求行时,ngx_proxy 模块针对 proxy_pass 指令做出了不同的处理:

  • URI 包含了变量,将变量解析,然后直接构造 URI 发送到上游;
  • URI 不含变量,没有指定 path 部分,使用 client 发来的原生的 path 部分拼接到 URI,然后发送到上游;
  • URI 不含变量,指定了 path,这里的处理是,把 decode 过的,client 发来的 URI 里的 path 部分,去掉和当前 location 匹配的部分后,转义(按 NGX_ESCAPE_URI 来操作)以后,和 proxy_pass 里指定的 URI 的 path 拼接,发送到上游,比如这样的配置:
location /foo {
    proxy_pass http://127.0.0.1:8082/bar;
}

如果 client 发来的 URI 里 path 是 /foo/%5B-%5D,最终上游的 URI path 会是 /bar/[-]。
因此我们在做 nginx conf 配置的时候,也需要小心考虑 URI 转义的问题。

ngx_lua 的 URI 转义机制

ngx_lua 提供的 ngx.escape_uri 函数,和 nginx 核心的转义机制也有一些差异(基于 ngx_lua v0.10.11),体现在对保留字符的处理上,ngx.escape_uri 底层使用的 ngx_http_lua_escape_uri,结构和 ngx_escape_uri 一致,而对应的 bitmap 不同。
对于整个 URI 的转义处理,在 ngx_escapeuri 的基础上,对 ‘”‘, ‘&’, ‘+’, ‘/’, ‘:’, ‘;’, ‘<‘, ‘=’, ‘>’, ‘[‘, ‘\’, ‘]’, ‘^’, ‘‘, ‘{‘ , ‘}’进行转义;对于 query 的处理,这里去掉了 & 的转义;对于 path + query 的处理,去掉了对 “‘”, “*”, “)”, “(“, “!” 的转义。目前 ngx.escape_uri 使用的是 NGX_ESCAPE_URI_COMPONENT ,从 PR 提交的信息来看,目前 ngx.escape_uri 的行为和 Chrome JS 实现的 encodeURIComponent 一致。
另外,ngx_lua 对 URI 的解码操作,除了它把 + 解码为空格以外,其他和 nginx 相同。

总结

在做相关的代理服务,网关服务时,URI 的编解码处理都是非常重要的,某些场景我们可能需要用 URI 来做 key,如果不处理好编解码问题,可能在 URI 复杂的情况下会达不到我们的预期效果,反而会浪费很多时间去排查问题的原因,特别地,在使用 nginx 和 ngx_lua 做服务时,我们更应该熟知它们处理 URI 编解码的区别,在理解它们的区别上做自身的业务处理,避免踩坑。

参考资料

参考资料