其他
ELK日常使用基础篇
本文字数:11149字
预计阅读时间:28分钟
前
言
在后端开发工程师的日常工作中,在遇到比如定位排查问题或是想要了解系统某些方面的情况时,会遇到以下的场景:
查询某个接口请求的日志。 查询某个服务的日志。 统计某些接口的每日调用数量以及时间分布。 统计某个或者某些接口每日的用户数量。
ELK(Elasticsearch+Logstash+Kibana)平台能很好的帮助我们完成上述工作,并且给我们提供了友好便利的用户界面,普遍应用于生产日志的查询分析中。所谓ELK用一句话概括就是:用Logstash收集日志或者数据到Elasticsearch存储起来并建立相关索引,再利用Kibana提供的查询界面到Elasticsearch上提供的索引进行查询和统计。
本文主旨是通过一个实际案例来讲述下ELK平台的日常操作方法,帮助我们的后台工程师能绕开枯燥的理论学习而直接收获实际应用的干货,进而帮助提高实际工作效率。
下面和大家说说如何利用Logstash收集日志(这里以nginx日志举例),其中会重点介绍filter模块中的grok插件,再会介绍工程师在实际工作中必须掌握的利用Kibana进行查询分析的几种典型场景。
示例代码中ELK使用的版本
Logstash 6.6.2 Kibana 6.6.2 Elasticsearch 6.6.2
1
Logstash收集日志
🔺1.1Logstash脚本文件
构成和常用插件
input Logstash的数据来源,可以是文件、Kafka、RabbitMQ、socket等等。 filter 从input接收到的数据经过filter进行数据类型转换、字段增减和修改、以及一些逻辑处理。虽然 filter模块是非必选的部分,但由于其可以将收集的日志格式化,合理的字段类型定义和辅助字段的创建可以使得以后的查询统计更加容易和高效。所以filter模块的配置是整个Logstash配置文件最重要的地方。 output 将filter得到的结果输出,可以是文件,Elasticsearch,Kafka等等。
input插件参考https://www.elastic.co/guide/en/logstash/6.6/input-plugins.html filter插件参考https://www.elastic.co/guide/en/logstash/6.6/filter-plugins.html output插件参考https://www.elastic.co/guide/en/logstash/6.6/output-plugins.html
input {
//input可以有多个插件用来处理不同的数据源;下面是一些常用的输入数据源
//从一个文件获取数据,可以支持单个或者多个文件
file {
path => "/opt/logs/app-*.log"
}
//从一个kafka的topic获取数据,同样支持多个topic
kafka {
#Kafka topic
topics => ["topic名字"]
#消费者组
group_id => "group名字"
bootstrap_servers => "kfk013218.heracles.sohuno.com:9092,..."
}
//从rabbitmq的一个队列获取数据
rabbitmq {
host => "rabbit host or ip"
port => 5672
queue => "queue名字"
vhost => "/vhost"
prefetch_count => 1
key => "routingkey.#"
exchange => "exchange名字"
codec => "json"
}
}
filter {
//filter模块内也提供了众多插件
//常用修改,字段拆分、连接、大小写、字段改名、类型变换
mutate {
split => ["message",","]#将读到的数据用’,‘ 拆分,后面会详细说
}
//日期类型的字段处理
//日期插件有区别于其他的插件,默认会改写当前数据记录所代表的时间(@timestamp这个字段),在通过kibana进行时间范围查询时会使用到@timestamp
date {
match => ["time", "yyyy-MM-dd HH:mm:ss.SSS"]
}
//强大的正则处理插件堪称Logstash中的神器组件
grok {
match => {
"params" => "Method:%{DATA:method},...TimeId:%{INT:timeId}"
}
}
//kv类型的字符串字段处理 params="key1=val1,key2=val2"
kv {
source => "params"
field_split => "="
value_split => ","
}
//处理json字符串字段 param="{a:1,b:2}"
json {
source => "param"
}
//丢弃接收到的数据
drop {}
}
output {
//debug 调试使用,生产环境禁止使用
stdout{codec => rubydebug}
//输出到Elasticsearch存储
elasticsearch {
hosts => ["ip:port",...]
index => "logstash-%{+YYYY.MM.dd}"//按天命名的index
}
file{
path => "/opt/logs/out-%{+YYYY-MM-dd}.log"//按天命名输出文件
}
}
🔺1.2利用logstash收集
nginx日志到Elasticsearch
input{
//同时收集多组nginx日志文件
file {
type => "api1-nginx-access"
path => ["/opt/logs/nginx/access.log"]
}
file {
type => "api2-nginx-access"
path => ["/opt/logs/nginx/access2.log"]
}
}
%{SYNTAX:SEMANTIC}
%{NUMBER:duration}
BASE10NUM (?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:\.[0-9]+)?)|(?:\.[0-9]+)))
NUMBER (?:%{BASE10NUM})
55.3.244.1 GET /index.html 15824 0.043
%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration}
# Networking
MAC (?:%{CISCOMAC}|%{WINDOWSMAC}|%{COMMONMAC})
CISCOMAC (?:(?:[A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4})
WINDOWSMAC (?:(?:[A-Fa-f0-9]{2}-){5}[A-Fa-f0-9]{2})
COMMONMAC (?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})
IPV6 ((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?
IPV4 (?<![0-9])(?:(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5]))(?![0-9])
IP (?:%{IPV6}|%{IPV4})
HOSTNAME \b(?:[0-9A-Za-z][0-9A-Za-z-]{0,62})(?:\.(?:[0-9A-Za-z][0-9A-Za-z-]{0,62}))*(\.?|\b)
IPORHOST (?:%{IP}|%{HOSTNAME})
HOSTPORT %{IPORHOST}:%{POSINT}
23/May/2019:14:40:10 +0800,1558593610.753,1660,0.028,"0.028",647,200,10.16.172.20-"123.126.70.235",POST /330000/v6/feeds/detail/query HTTP/1.1,"app_key_vs=2.6.0&appid=330000&feed_count=10&feed_id=507297347911306752&flyer=1558593610700&idfa=A6719238-5AF6-4B57-B4B6-676B0905704D&log_user_id=248137098937342464&query_type=6&sig=28d2c189144c5380113afff158ea257d&since_time_comment=3000-01-01%2001%3A01%3A01.000&since_time_pure=3000-01-01%2001%3A01%3A01.000",UPS/"10.18.76.18:8080","sns/2.6.0 (com.sohu.sns; build:3; iOS 12.1.4) Alamofire/1.0",-,cs-ol.sns.sohu.com,"01374622096363527552","248137098937342464","872289029629325312@sohu.com","110501"
log_format nginx_nobody_log '$time_local,$msec,$request_length,$request_time,"$upstream_response_time",$body_bytes_sent,$status,$remote_addr-"$http_x_forwarded_for",$request,"$request_body",UPS/"$upstream_addr","$http_user_agent",$http_referer,$host,"$http_s_cid","$http_s_pid","$http_s_ppid","$http_p_appid"';
SNS_NGINX_ACCESS %{HTTPDATE:time_local},%{NUMBER:msec},%{INT:request_length},%{BASE16FLOAT:request_time},"(?:-|%{BASE16FLOAT:upstream_response_time})(,%{NUMBER:upstream_response_time2})?",%{INT:body_bytes_sent},%{INT:status},%{IPORHOST:remote_addr}-"%{DATA:http_x_forwarded_for}(, %{DATA:http_x_forwarded_for2})?",%{WORD:method} %{URIPATH:interface}(?:%{DATA:uri_param})? HTTP/%{NUMBER:http_version},"%{DATA:request_body}",UPS/"%{DATA:upstream_addr}(, %{DATA:upstream_addr2})?","%{DATA:http_user_agent}",%{DATA:http_referer},%{IPORHOST:host},"%{DATA:http_s_cid}","%{DATA:http_s_pid}","%{DATA:http_s_ppid}","%{DATA:http_p_appid}"
grok {
patterns_dir => "/opt/conf/logstash-pattern"
match => {
"message" => "%{SNS_NGINX_ACCESS}"
}
}
filter {
//if判断
if "form-data" in [message] {
drop {}
}
//正则匹配,patterns_dir 里面的文本文件都会读取出来
grok {
patterns_dir => "/opt/conf/logstash-pattern"
match => {
//message表示读取到的nginx一行日志,使用编辑好的pattern文件中名为SNS_NGINX_ACCESS进行正则匹配
"message" => "%{SNS_NGINX_ACCESS}"
}
//删除message字段,message字段是默认的字段记录的是原始nginx日志的一整行,删除后可以减少es的存储占用
remove_field => ["message"]
}
//将time_local日期类型的字符串转换成日期类型,并赋值给target指向的字段,默认是@timestamp字段
date {
match => ["time_local","dd/MMM/YYYY:HH:mm:ss Z"]
target => "@timestamp" //这句可以不写
}
if "-" not in [http_x_forwarded_for] {
//ip地址物理位置处理 GEO geographical location of IP addresses
//ip数据库参考这里 https://dev.maxmind.com/geoip/geoip2/geolite2
geoip {
source => "http_x_forwarded_for"
fields => ["ip","city_name", "country_code3", "country_name", "ip", "latitude", "longitude","location"]
}
}
//将所有字段进行urldecode
urldecode {
all_fields => true
}
//kv 类型的字段处理
//处理前 uri_param="?app_key_vs=2.6.0&appid=330000&feed_count=10&feed_id=xxx"
kv {
source => "uri_param"
field_split => "&?"
value_split => "="
include_keys => [ "appid", "user_id"]
}
//针对request_body字段同样使用kv处理
if [request_body] != '-' {
kv {
source => "request_body"
field_split => "&?"
value_split => "="
include_keys => [ "appid", "user_id" ]
}
}
mutate {
//字段重命名
rename => [ "appId", "appid" ]
//字段类型转换
convert => ["request_time", "float", "upstream_response_time", "float"]
//删除port字段
remove_field => ["port"]
//将host字段切分成hostname和http_port两个字段
split => ["host", ","]
add_field => {"hostname" => "%{[host][0]}"}
add_field => {"http_host" => "%{[host][1]}"}
}
}
//最后就是按天存储到Elasticsearch索引中
output {
//type 是input中file模块定义的type,这样就可以区分区是从哪个access日志读取的。
//将不同的access日志存储在不同的索引中
if [type] == "api1-nginx-access" {
elasticsearch {
hosts => ["es ip:port"...]
index => "logstash-nginx-access1-%{+YYYY.MM.dd}"
}
}
if [type] == "api2-nginx-access" {
elasticsearch {
hosts => ["es ip:port"...]
index => "logstash-nginx-access2-%{+YYYY.MM.dd}"
}
}
}
2
Elasticsearch的RESTful API
🔺2.1 RESTful API介绍
🔺2.2 常用RESTful API使用
GET /
GET logstash-nginx-access*/
"mappings" : {
...
"properties" : { //mappings 都有哪些属性
"@timestamp" : { // @timestamp字段是日期类型
"type" : "date"
},
"@version" : {
"type" : "keyword" //keyword表示全文字段,查询时不分词检索
},
"body_bytes_sent" : {
"type" : "long"
},
"geoip" : { //嵌套的document类型
"dynamic" : "true", //动态 mapping ,表示geoip可能会有新的字段
"properties" : {
"city_name" : {
"type" : "text",
"norms" : false, //在查询的score不考虑city_name字段
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256 //city_name的keyword只保留256字节长度
}
}
},
......
"ip" : {
"type" : "ip"
},
"latitude" : {
"type" : "half_float" //16-bit 浮点数
},
"location" : {
"type" : "geo_point"
},
"longitude" : {
"type" : "half_float"
}
}
},
"request_time" : {
"type" : "float" //32-bit 浮点数
}......
}
}
🔺2.3 Search APIs的常见用法
GET logstash-nginx-access*/_search
{
"took" : 205, //查询所花费的毫秒数
"timed_out" : false,
"_shards" : {
"total" : 8, //查询了几个分片
"successful" : 8, //查询执行成功的分片数
"skipped" : 0,
"failed" : 0
},
"hits" : { //查询命中的数据
"total" : 96198672, //indexpattern下的查询命中的全部数据条数
"max_score" : 1.0,
"hits" : [
......]
}
}
GET logstash-nginx-access*/_search?q=status:200&size=15&sort=@timestamp:desc
GET logstash-nginx-access*/_search
{
"size":15,
"query":{
"term":{
"status":200
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
{
"size":15,
"_source": [ "interface", "remote_addr" ],
"query":{
"term":{
"status":200
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
"_source": {
"includes": [ "http*", "geoip.*" ], //包含的字段,http开头的字段,geoip的嵌套类型字段
"excludes": [ "xxx" ] //排除哪些字段
}
"_source": false, //不显示_source内容
GET logstash-nginx-access*/_count
{
"query":{
"term":{
"status":200
}
}
}
{
"count" : 3807,
"_shards" : {
"total" : 8,
"successful" : 8,
"skipped" : 0,
"failed" : 0
}
}
GET logstash-nginx-access*/_search?scroll=5m
{
"size":15,
"_source": [ "interface", "remote_addr" ],
"query":{
"term":{
"status":200
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
{
"_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoCAAAAAACICIZFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAABkXxNxZSbDRKeE9hLVNqdTVnVlUtc2h3MElBAAAAAAqefhsWR2R3NVNhel9RNU8tN0dNTm16dzE0QQAAAAAA2LfBFnJsckEyWVkzUUNhb2JHOUZTQ0IzQlEAAAAAAiAiGhZfQlFiSXVlQ1M1U3ZXQ0IzWDd2RFN3AAAAAADYt8IWcmxyQTJZWTNRQ2FvYkc5RlNDQjNCUQAAAAACICIbFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAADd5U1hZ3djgxRnFRMVFZZWdwSE1BZU1oOWp3",
"took" : 249,
"timed_out" : false,
"_shards" : {
"total" : 8,
"successful" : 8,
"skipped" : 0,
"failed" : 0
},
"hits" : {......
GET /_search/scroll
{
"scroll":"5m",
"scroll_id":"DnF1ZXJ5VGhlbkZldGNoCAAAAAACICIZFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAABkXxNxZSbDRKeE9hLVNqdTVnVlUtc2h3MElBAAAAAAqefhsWR2R3NVNhel9RNU8tN0dNTm16dzE0QQAAAAAA2LfBFnJsckEyWVkzUUNhb2JHOUZTQ0IzQlEAAAAAAiAiGhZfQlFiSXVlQ1M1U3ZXQ0IzWDd2RFN3AAAAAADYt8IWcmxyQTJZWTNRQ2FvYkc5RlNDQjNCUQAAAAACICIbFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAADd5U1hZ3djgxRnFRMVFZZWdwSE1BZU1oOWp3"
}
DELETE /_search/scroll
{
"scroll_id" : "DnF1ZXJ5VGhlbkZldGNoCAAAAAACICIZFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAABkXxNxZSbDRKeE9hLVNqdTVnVlUtc2h3MElBAAAAAAqefhsWR2R3NVNhel9RNU8tN0dNTm16dzE0QQAAAAAA2LfBFnJsckEyWVkzUUNhb2JHOUZTQ0IzQlEAAAAAAiAiGhZfQlFiSXVlQ1M1U3ZXQ0IzWDd2RFN3AAAAAADYt8IWcmxyQTJZWTNRQ2FvYkc5RlNDQjNCUQAAAAACICIbFl9CUWJJdWVDUzVTdldDQjNYN3ZEU3cAAAAADd5U1hZ3djgxRnFRMVFZZWdwSE1BZU1oOWp3"
}
GET logstash-nginx-access*/_search
{
"size":15,
"_source": [ "interface", "remote_addr","status" ],
"query":{
"term":{
"status":200
}
},
"highlight": {
"fields": {"status":{}}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
aggs表示聚合查询; “day_count”是我们自定义的一个聚合的名称(aggs可以有多个和多层所以需要指定一个名称); date_histogram表示是一个日期分布器,是按照“@timestamp”这个日期字段按照1d(1天)的时间间隔进行分布的; size:0 表示不返回具体的记录,hits部分是空数组
GET logstash-nginx-access*/_search
{
"aggs": {
"day_count": {
"date_histogram": {
"field": "@timestamp",
"interval": "1d"
}
}
},
"size": 0
}
GET logstash-nginx-access*/_search
{
"aggs": {
"day_count": {
"date_histogram": {
"field": "@timestamp",
"interval": "1d"
}
}
},
"size": 0,
"query": {
"bool": {
"must": [
{"range": { //一个range查询
"@timestamp": { //查询1月20到1月25时间范围
"gte": "2020-01-20",
"lte": "2020-01-25",
"format": "yyyy-MM-dd"
}
}
},{
"match": { //只匹配status=200的记录
"status": 200
}
}
]
}
}
}
{
"aggs": {
"interface_count": {
"terms": {
"field": "interface.keyword",
"size": 5
}
}
},
"size": 0,
"query": {
"bool": {
"must": [
{"range": {
"@timestamp": {
"gte": "2020-05-20",
"lte": "2020-05-21",
"format": "yyyy-MM-dd"
}
}
},{
"match": {
"status": 200
}
}
]
}
}
}
"aggs": {
"interface_count": {
"terms": {
"field": "interface.keyword",
"size": 5
}
,
"aggs" :{
"hours" : {
"date_histogram": {
"field": "@timestamp",
"interval": "1h" //按照1小时间隔分布
}
}
}
}
3
利用kibana进行日志查询与分析
“字段名” 和 “字段名.keyword” 两种过滤方式
单字段多词查询
多过滤条件查询
查询过程中的过滤条件快速修改
🔺3.2 在Kibana中使用
kuery-query和Lucene进行高阶查询
全文搜索:
直接在搜索框输入想查询的内容:${content} 或者 "${content}"
<span style="color: rgb(51,51,51);">引号在这里起到的作用是对搜索内容的限制,如果不写引号,那么搜索内容将按照分词处理,不区分内容顺序,比如你可能得到"quick fox brown"这样的结果
通配符:
? 匹配单个字符,如app?d
* 匹配0到多个字符,如searc*h
注意通配符都不可用作表示第一个字符,如*test,?test
正则搜索:
支持简单的正则搜索,但效率很低,不推荐使用,使用时用"/"引用
[name:/joh?n(ath[oa]n)/](http://name/joh?n(ath[oa]n)/)
模糊搜索:
quikc~ brwn~ foks~
~:在一个单词后面加上~启用模糊搜索,可以搜到一些拼写错误的单词
first~ 这种也能匹配到 frist
还可以设置编辑距离(整数),指定需要多少相似度
cromm~1 会匹配到 from 和 chrome
默认2,越大越接近搜索的原始值,设置为1基本能搜到80%拼写错误的单词
近似搜索:
在短语后加上~,可以搜到被隔开或顺序不同的单词
"fox quick"~5,表示fox和quick中间可以隔5个单词,可以搜索到"quick brown fox"
范围搜索:
All days in 2012:
date:[2012-01-01 TO 2012-12-31]
Numbers 1..5
count:[1 TO 5]
Tags between alpha and omega, excluding alpha and omega:
tag:{alpha TO omega}
Numbers from 10 upwards
count:[10 TO *]
Dates before 2012
date:{* TO 2012-01-01}
1~5,但不包括5
[1,5}
可以简化成以下写法:
age:>10
age:<=10
age:(>=10 AND <20)
age:(+>=10 +<20)
优先级:
使用^表示,使一个词语比另一个搜索优先级更高,默认为1,可以为0~1之间的浮点数,来降低优先级
quick^2 fox
逻辑操作:
AND OR + -
+:搜索结果中必须包含此项
-:不能含有此项
+appid -s-ppid aaa bbb ccc:结果中必须存在appid,不能有s-ppid,剩余部分尽量都匹配到
逻辑操作符kibana6也支持写为 && ,|| ,!,但不推荐使用
逻辑组合示例:((quick AND fox) OR (brown AND fox) OR fox) AND NOT news
转义特殊字符:
+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
以上字符当作值搜索的时候需要用\转义,其中< 和 > 根本无法规避,如果包含这两个字符只能另寻他法
\(1\+1\)\=2用来查询(1+1)=2
🔺PS:如何在Kibana增加索引
也许你还想看
(▼点击文章标题或封面查看)
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛