目前最为主流的容器编排工具主要有kubernetes、mesos、swarm,个人不评价谁好谁坏因为每个东西都有自己的优势。不过个人认为目前关注度最高的应该当属kubernetes,现在越来越多的公司采用kubernetes作为底层编排工具开发自己的容器调度平台。既然是一个PAAS平台那么就应该提供一个计算监控等一体的服务,因为是在kubernetes运行上面的容器大多数都是无状态服务,所以统一的日志管理又是其中必不可少的一部分。下面我们就讲一下如何基于filebeat开发属于自己的日志采集。
目前用的最多的日志管理技术应该是ELK,E应该没有太多的疑问基本上很多公司都是采用的这个作为存储索引引擎。L及logstash是一个日志采集工具支持文件采集等多种方式,但是基于容器的日志采集又跟传统的文件采方式略有不同,虽然docker本身提供了一些log driver但是还是无法很好的满足我们的需求。现在kubernetes官方有一个日志解决方案是基于fluentd的。至于为什么最后选择采用filebeat而没有用fluentd主要有一下几点:
- 首先filebeat是go写的,我本身是go开发,fluentd是ruby写的很抱歉我看不太懂
- filbeat比较轻量,filbeat现在功能虽然比较简单但是已经基本上够用,而且打出来镜像只有几十M
- filbeat性能比较好,没有具体跟fluentd对比过,之前跟logstash对比过确实比logstash好不少,logtash也是ruby写的我想应该会比fluentd好不少
- filbeat虽然功能简单,但是代码结构非常易于进行定制开发
- 还有就是虽然用了很久fluentd但是fluentd的配置文件实在是让我很难懂
filebeat如何采集kubernetes日志
所以基于以上几点决定采用filebeat开发了自己的日志采集。
filebeat的Github地址是https://github.com/elastic/beats
里面囊括了好几个项目其中就包括filebeat。
和其他的日志采集处理一样filebeat也有几个部分分别是input、processors、output,不过filebeat提供的能力还比较少,不过无所谓够用就好。
filebeat提供了一个add_kubernetes_metadata
的processor,文件的采集路径就要配成/var/lib/docker/containers/*/*-json.log
主要是监听kubernetes的apiserver把容器对应的pod的信息存到内存里面,从文件日志source里面(就是上面的那个路径)里面获取容器id匹配得到pod的信息。
因为json.log文件里面的日志都是json格式的所以需要对日志进行json格式化,filebeat有一个processor叫decode_json_fields
这些processor都支持条件判断,可以通过条件判断来绝对是否要对某一条日志进行处理。filebeat默认的日志字段是message但是*-json.log解析出来以后的日志字段是log,如果同时配置了其他的日志采集这个时候所用的存储日志的字段就不一样了,所以需要对它们进行处理让它们使用同一个字段,但是filebeat并没有提供这个功能所以自己写了一个add_fields
的功能。
整理后的配置文件如下:
filebeat.prospectors:
- type: log
paths:
- /var/lib/docker/containers/*/*-json.log
- /var/log/containers/applogs/*
processors:
- add_kubernetes_metadata:
in_cluster: false
host: "127.0.0.1"
kube_config: /root/.kube/config
- add_fields:
fields:
log: '{message}'
- decode_json_fields:
when:
regexp:
log: "{*}"
fields: ["log"]
overwrite_keys: true
target: ""
- drop_fields:
fields: ["source", "beat.version", "beat.name", "message"]
- parse_level:
levels: ["fatal", "error", "warn", "info", "debug"]
field: "log"
logging.level: info
setup.template.enabled: true
setup.template.name: "filebeat-%{+yyyy.MM.dd}"
setup.template.pattern: "filebeat-*"
#setup.template.fields: "${path.config}/fields.yml"
setup.template.fields: "/fields.yml"
setup.template.overwrite: true
setup.template.settings:
index:
analysis:
analyzer:
enncloud_analyzer:
filter: ["standard", "lowercase", "stop"]
char_filter: ["my_filter"]
type: custom
tokenizer: standard
char_filter:
my_filter:
type: mapping
mappings: ["-=>_"]
output:
elasticsearch:
hosts: ["127.0.0.1:9200"]
index: "filebeat-%{+yyyy.MM.dd}"
如果线上环境filebeat也是以daemonset的方式运行在kubernetes集群里面,所以in_cluster
就需要设置成true,对应的kube_config
则不需要配置了,host参数则是监听的某一个节点的pod,所以这个值应该是filebeat运行所在节点的pod的名称,当然也可以不写,那样的话就是监听全局的pod,不过这个对于filebeat来说是没必要的也是不好的。
add_fields
processor可以添加自己想要的字段,值可以是字符串也可以是{message}
格式,如果是这种格式则会从已有的字段里面取值进行填充。
parse_level
processor是用于一个匹配日志格式的功能,如果日志文件最前面出现的那个日志级别则这个日志加一个相应级别的字段。
filebeat还有对于template处理的功能的功能可以指定所用的mapping。
开发filebeat processor
使用的过程中主要是针对一些不满足的processor进行了开发,filebeat的代码结构非常清晰抽象也很好,可以很简单的进行开发。
filebeat的processor功能主要放在libbeat和filbeat同级的目录下,在这个目录下就叫processors。可以看到里面有actions
,add_cloud_metadata
、add_kubernetes_metadata
、add_docker_metadata
所以filebeat也只支持直接docker的processor的,比较普通的processor都是放在actions下面的所以如果我们需要开发一些简单的processor的话可以直接放到下面,包括decode_json和drop_event等也是放在下面的。以add_field
为例:
package actions
import (
"fmt"
"regexp"
"strings"
"github.com/elastic/beats/libbeat/beat"
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/processors"
)
type addFields struct {
Fields map[string]string
reg *regexp.Regexp
}
func init() {
processors.RegisterPlugin("add_fields",
configChecked(newAddFields,
requireFields("fields"),
allowedFields("fields", "when")))
}
func newAddFields(c *common.Config) (processors.Processor, error) {
config := struct {
Fields map[string]string `config:"fields"`
}{}
err := c.Unpack(&config)
if err != nil {
return nil, fmt.Errorf("fail to unpack the add_fields configuration: %s", err)
}
f := &addFields{Fields: config.Fields, reg: regexp.MustCompile("{(.*)}")}
return f, nil
}
func (f *addFields) Run(event *beat.Event) (*beat.Event, error) {
var errors []string
for field, value := range f.Fields {
matchers := f.reg.FindAllStringSubmatch(value, -1)
if len(matchers) == 0 {
event.PutValue(field, value)
} else {
if len(matchers[0]) >= 2 {
val, err := event.GetValue(strings.Trim(matchers[0][1], " "))
if err != nil {
errors = append(errors, err.Error())
} else {
event.PutValue(field, val)
}
}
}
}
return event, nil
}
func (f *addFields) String() string {
var fields []string
for field, _ := range f.Fields {
fields = append(fields, field)
}
return "add_fields=" + strings.Join(fields, ", ")
}
需要定义自己的struct, newAddFields方法通过配置文件初始化自己的struct。并在init里面通过RegisterPlugin把自己的processor注册进去。这个struct主要是要实现Run方法,这个方法就是对于每一条日志event的具体处理。
到这就基本上实现了对接kubernetes的对接改造就基本上完成了,当然还有其他很多工作可以做,比如golang本身的regex和encoding/json性能比较差,这些都是可以优化的地方。
我自己fork出来的地址是https://github.com/yiqinguo/beats
增加了Makefile直接编译打镜像,和filebeat-ds.yml直接发到kubernetes集群里面。