Filebeat+Kafka+数据处理服务+Elasticsearch+Kibana+Skywalking日志收集系统

3,879 阅读16分钟

前言

该文档不会为读者提供中间件的安装和部署教学,仅作为我在这大半年从零开始建立日志收集系统的整个过程。整个日志收集系统目前已迭代三次,从第二版开始已经稳定运行半年,第三版的升级是锦上添花,继续深挖日志收集的可能性。

读者能从本文看到一条从零开始的日志收集系统的建立通路,能看到我选择和处理各个中间件的思考和碎碎念。该方案是基于EFK的一个通用框架,由于数据处理服务的存在,使得该方案能兼容绝大部分奇奇怪怪的日志,只要能收集过来,那处理就是多几行代码的问题。我个人推荐有条件的读者可以尝试自己去搭建这样一套完整的日志收集系统,整个做下来会让你对中间件和日志收集的思考更加深刻。

日志收集第一版

设计方案

基于注解切面+Kafka+数据处理服务+ES

通过手动填写注解的字段信息来填充需要的日志信息,在切面异步通过Kafka传递消息,然后数据处理服务消费消息,经过处理后批量插入到ES。后来觉得这样收集太冗余,很少有同事愿意配置些东西或者写个注解。于是排除了公司的默认日志切面,自己写了切面来传递有默认值的数据

思考

当时写这第一版方案是因为公司原本有一套老的ELK来收集日志,以Log4j2+Kafka+ELK方式实现。我当时这个基于注解和切面的想法原本只是想做个补充,类似于行为收集的入口。后来发现给同事用的时候,注解参数基本都是用默认,也不会写,毕竟每个接口上来个注解,写一堆参数,换我我也不太想写。但是这个思路是可以的,作为补充是合适的

不足

1.注解参数配置太多,不想配

2.耦合性高,注解一旦变动涉及修改太多

3.Log4j2的Kafka相关配置全部默认,没有做高并发配置

4.ES索引没有优化,单索引过大问题等等

代码实现

切面如下

@Aspect
@Component
@EnableConfigurationProperties(value = {TransferProperties.class})
@Slf4j
public class LogTransferHandler {
    public static final String UAT_DEFAULT_TOPIC = "ptm-uat-request";
    public static final String PRO_DEFAULT_TOPIC = "ptm-pro-request";
    @Autowired
    //从配置文件种配置的一些默认数据
    private TransferProperties transferProperties;
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 带有注解@LogCollector和controller层组成切点
     */
    @Pointcut("@annotation(com.ruijie.transfer.annotation.LogCollector)||execution(* *.ruijie..*.controller..*.*(..))")
    public void pointCut() {
    }

    @Resource
    private BaseEnvironmentConfigration baseEnvironmentConfigration;

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //proceed方法用于启动目标方法执行,并能获取返回值信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        Instant start = Instant.now();
        Object result = joinPoint.proceed();
        Instant end = Instant.now();
        //从threadlocal中获取用户信息
        String userId = RequestContext.getCurrentContext().getUserId();
        String userName = RequestContext.getCurrentContext().getUserName();
        String methodName = methodSignature.getName();
        //log4j2日志信息录入
        if (StringUtils.isEmpty(MDC.get("user_id")) && !StringUtils.isEmpty(userId) && !"null".equalsIgnoreCase(userId)) {
            MDC.put("user_id", userId);
        }
        //...省略代码--业务代码
        //通过log4j2直接传递日志,该处通过AOP收集稍显鸡肋,不过暂时保留
        if (isEsCollect) {
            CompletableFuture.runAsync(() -> {
                //...省略代码--组装日志参数
                String msg = JSON.toJSONString(provider);
                String topic;
                if ("pro".equals(baseEnvironmentConfigration.getCurrentEnv())) {
                    topic = PRO_DEFAULT_TOPIC;
                } else {
                    topic = UAT_DEFAULT_TOPIC;
                }
                ListenableFuture<SendResult<String, String>> sendListener = kafkaTemplate.send(topic, msg);
                sendListener.addCallback(success -> {
                    log.info("日志传递成功,方法={},操作人={}", provider.getRequestUrl(), provider.getUserId());
                }, err -> {
                    log.error("日志传递失败,方法={},操作人={}", provider.getRequestUrl(), provider.getUserId());
                });
            }).handle((res, e) -> {
                if (e != null) {
                    log.error("日志传递出现异常", e);
                }
                return res;
            });
        }
        return result;
    }
    //...省略代码--入参处理方法

日志收集第二版

设计方案

Log4j2+Kafka+数据处理服务+Elasticsearch+Kibana

在保留第一版行为日志收集方案雏形的基础上进行了默认日志上的改造,首先是干掉了公司的日志收集模块,因为公司是以组件方式提供,所以排除也很简单,依赖去掉就行了。重写Log4j2的配置文件,根据环境进行了日志的隔离,优化了Kafka的配置 ,ES部分索引进行了优化,该方案已支持千万级的日志收集,时延保持在5s内,运行半年暂未发现日志丢失情况,最重要的是以组件方式加载,无侵入。

思考

公司提供的日志收集,实在是不好用,全公司的项目都混在一块,不分开发测试正式环境,索引也没有优化,字段类型全是text,也没分词,总的来说就是不如linux命令舒服。在我推动我们组领导去搞一套日志收集系统之前,所有同事都不用公司提供的elk查询日志,都是linux命令查询,这不废话嘛,没分词怎么查日志,索引的字段设置也有点奇怪,有些冗余字段也不知道干啥的。跟架构组反馈了很多次,一直说要改,方案变动了两版,就是一直没东西出来。半年过去我自己的日志收集系统都迭代了三版了,第二版也正常运行了小半年了,果然指望别人不如自己。

做全套的日志收集嘛,第一时间也是迷茫的,怎么做,之前就听说了一个词叫ELK,关键是怎么做这个ELK才能变成日志啊。然后就上网搜一下,大致分为两种,第一种就是传统的ELK或者EFK,第二种是相对于ELK轻量的日志框架,Loki+Promtail+Grafana。因为我有现成的ElasticSearch,并且做第一版方案的时候已经学习了相关知识,所以我还是倾向于第一种,毕竟公司给的服务器配置不差,三节点的Kafka集群也是现成的。

做的时候,由于第一版方案用Kafka+数据处理服务+ES是一条通路,所以为了尽快做出成品给领导看看效果,我还是准备在这条路上拓宽加深。时间上确实存在问题,因为我个人是组里的开发主力,新项目总是跑不了,还有几个老项目要运维,同时我还有别的组件(比如前文提到的工具组件,业务组件,单点登录组件等)需要维护,同事遇到问题也要问我,所以我都是尽量在非工作时间来做这个。

由于时间的问题,首先干掉了Logstash,原因一是我不想花时间研究Logstash的安装与配置,原因二是ElasticSearch的动态索引以及批量插入等在第一版方案中已经积累了代码了,所以我直接复用了,并且我对Kafka和ElasticSearch做了配置上的优化,从时效上来说秒级的对于日志来说肯定是没什么问题的。日志收集的第一步收集肯定不能用切面了,局限性太大,我翻了下Log4j2的官方文档,发现直接就支持推送Kafka,于是乎,收集这一步也定了,最后展示这一步,之前第一版是通过接口查询的,后来发现Kibana实在是太香了,建个索引模板就好了,展示也挺好看的。所以整体方案就下来了,Log4j2+Kafka+数据处理服务+Elasticsearch+Kibana。

代码实现

Log4j2优化

Log4j2的优化点主要是减少无用日志的输出,少传点少占地方。原本是想用markfilter匹配字符串的模式排除掉环境变量为开发环境的数据,结果自己上手试了下,发现不好使,用了正则匹配啥的也不好使,整了两三天,官方文档看遍了,网上例子找遍了最后还是排不了。索性采取终极方案,log4j2.xml这个配置文件我也分环境,搞了两,一个log4j2-dev.xml一个log4j2.xml,通过配置文件application-dev.properties开发环境中配置logging.config=classpath:log4j2-dev.xml来避免输出开发环境的日志。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="FATAL" monitorInterval="300">
    <properties>
        取配置文件中的参数
        <property name="APPLICATION">${bundle:application:spring.application.name}</property>
        Kafka配置
        <property name="KAFKA_SERVERS">xxx,xxx,xxx</property>
        取系统环境变量env,这里注意容器环境配置
        <property name="KAFKA_TOPIC">log-zero-${sys:env}</property>
        日志文件存放地点
        <property name="LOG_HOME">/data/logs</property>
        <property name="LOG_FILE_NAME">${APPLICATION}</property>
        <property name="CHARSET">UTF-8</property>
        控制台打印简略信息,日志文件和推送Kafka消息采用全字段,%X{xxx}是通过切面或者拦截器使用MDC注入的
        <property name="CONSOLE_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS}{GMT+8}|%p|${APPLICATION}|${sys:env}|%X{request_ip}|%traceId|%X{request_url}|%c%X{real_method}|%m%n</property>
        <property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS}{GMT+0}|%p|${APPLICATION}|${sys:env}|${sys:local-ip}|%X{request_ip }|%traceId|%X{request_url}|%X{request_method}|%c%X{real_method}|%X{user_id}|%X{user_name}|%t|%m%n</property>
        <property name="KAFKA_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS}{GMT+8}|%p|%m%n</property>
    </properties>
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="${CONSOLE_PATTERN}" charset="${CHARSET}"/>
        </Console>
        滚动错误日志,额外记录一份专门的错误日志
        <RollingRandomAccessFile name="error" fileName="${LOG_HOME}/${APPLICATION}-error.log"
                                 filePattern="${LOG_HOME}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}-%i-error.log.bak"
                                 immediateFlush="false" append="true">
            <PatternLayout pattern="${LOG_PATTERN}" charset="${CHARSET}"/>
            <Filters>
                <ThresholdFilter level="fatal" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingRandomAccessFile>
        <RollingRandomAccessFile name="file" fileName="${LOG_HOME}/${LOG_FILE_NAME}.log"
                                 filePattern="${LOG_HOME}/${LOG_FILE_NAME}.%d{yyyy-MM-dd-HH}-%i.log.bak"
                                 immediateFlush="false" append="true">
            <PatternLayout pattern="${LOG_PATTERN}" charset="${CHARSET}"/>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="24"/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingRandomAccessFile>
        Kafka推送日志,这里是高并发情况的配置,最大消息要配置,不然堆栈信息传不了
        <Kafka name="ZeroKafka" topic="${KAFKA_TOPIC}" syncSend="true" ignoreExceptions="false">
            <PatternLayout pattern="${LOG_PATTERN}" charset="${CHARSET}"/>
            <Property name="bootstrap.servers">${KAFKA_SERVERS}</Property>
            <Property name="max.block.ms">2000</Property>
            <Property name="batch.size">163840</Property>
            <Property name="linger.ms">20</Property>
            <Property name="buffer.memory">67108864</Property>
            <Property name="max.request.size">10485760</Property>
            <Property name="request.timeout.ms">60000</Property>
            <Property name="compression.type">lz4</Property>
        </Kafka>
        Kafka的错误日志也打印一下
        <Failover name="failover" primary="ZeroKafka" retryIntervalSeconds="600">
            <Failovers>
                <AppenderRef ref="failoverKafkaLog"/>
            </Failovers>
        </Failover>
        <RollingRandomAccessFile name="failoverKafkaLog" fileName="${LOG_HOME}/${APPLICATION}-kafka.log"
                                 filePattern="${LOG_HOME}/${APPLICATION}-kafka-%d{yyyy-MM-dd-HH}-%i.log.bak"
                                 immediateFlush="false" append="true">
            <PatternLayout pattern="${KAFKA_PATTERN}" charset="${CHARSET}"/>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="24"/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingRandomAccessFile>
        skywalking推送日志信息和链路追踪配置,需要在jvm参数配置skywalking探针
        <GRPCLogClientAppender name="grpc-log">
            <PatternLayout pattern="${LOG_PATTERN}" charset="${CHARSET}"/>
        </GRPCLogClientAppender>
    </Appenders>
    <Loggers>
        <asyncRoot level="info" includeLocation="false">
            <AppenderRef ref="file"/>
            <AppenderRef ref="error"/>
            <AppenderRef ref="console"/>
            <AppenderRef ref="failover"/>
            <AppenderRef ref="grpc-log"/>
        </asyncRoot>
        <logger name="org.springframework" level="info"/>
        <logger name="org.mybatis.spring.SqlSessionUtils" level="info"/>
        <logger name="org.apache.kafka" level="info"/>
    </Loggers>
</Configuration>

Kafka优化

Kafka这部分的优化存在于两个地方,一个是Log4j2的生产侧,一个是数据处理服务的消费侧。

生产者的配置位于Log4j2.xml配置文件中,这个看log4j2官网,可以看哪些参数可以配置。

数据处理服务消费者的配置,我用的是spring boot环境进行开发,使用的也是spring提供的kafkaTemplat。但是我没有直接在配置文件配置Kafka相关的参数,因为那就定死了,我是在代码里面自定义了几种类型的消费者,比如高性能,高可用,低时延等,我在另一个组件里面已经封装了,所以这里直接用就可以了。

这里选择的是高吞吐量的配置类型。因为日志的话不考虑低时延和丢失问题只需要保持高吞吐量,所以首先批量大小增加,时延也增加,减少发送次数,批量大小上去了对应缓冲区大小也要增加,消息大小考虑到部分堆栈信息会超过1MB,也调大一点,acks默认1也可以,不怕丢失改为0也行,最后加上压缩lz4,这是生产者配置。消费者的配置是,单次拉取数据调大,默认自动提交偏移量,批量接收信息,选择并发消费。

topic提前要建好,分区数等于broker数就行,副本1或3,其他不需要特别配置,总的来说日志的场景对于Kafka来说是个经典的高吞吐量场景。

Kafka主题配置是动态的,这里插入spEl表达式取系统环境变量,消费者选择高性能类型

Kafka主题配置是动态的,这里插入spEl表达式取系统环境变量,消费者选择高性能类型
@KafkaListener(topics = {"log-zero-#{systemProperties['env']}"}, containerFactory = "performanceFactory")
public void kafkaLogListen(List<ConsumerRecord<String, String>> records) {
    List<LogMsgDTO> logMsgList = new ArrayList<>(1024);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
    records.forEach(record -> {
        String value = record.value();       
        String[] split = value.split("\\|");
        String system = split[2];
        按照月份动态的创建索引,这里根据日志量情况自行选择
        String topic = record.topic() + "-" + formatter.format(LocalDate.now());
        boolean existIndex = esService.existIndex(topic + "-alias1");
        if (!existIndex) {
            esService.createIndex(topic + "-alias1", topic, false);
        }
         按照约定好的日志格式进行数据处理
        if (split.length > 13) {
            String env = split[3];
            LogInfoDTO info = new LogInfoDTO();
            info.setCreateTime(split[0]);
            //...省略代码--填充ES文档数据
            logMsgList.add(new LogMsgDTO(topic + "-alias1", JSON.toJSONString(info)));
        }
    });
    if (!CollectionUtils.isEmpty(logMsgList)) {
        esService.bulkAddRequest(logMsgList);
    }
}

ElasticSearch优化

天元突破-ElasticSearch性能究极进化

优化的点主要是,索引分片数是集群节点数的两倍即可,副本给1,考虑到日志量比较大,我是按月来建索引,一个月目前接入的几个关键系统产生的日志量大概是几十G,做了一个定时任务删除12个月以上的日志,防止硬盘爆满。插入日志的时候,选择批量插入,设置超时时间长点,防止数据没传完。在索引的字段属性选择上,尽量选择keyword,能够更好的利用倒排索引进行查询优化,最关键的是分词器不能默认,用ik中文分词器,默认的太难用。

/**
 * 是否存在索引

* @param index 索引
 */
public boolean existIndex(String index) {
    List<String> allZeroIndex = findAllZeroIndex();
    if (!CollectionUtils.isEmpty(allZeroIndex)) {
        return allZeroIndex.contains(index);
    } else {
        GetIndexRequest request = new GetIndexRequest(index);
        try {
            return zeroClient.indices().exists(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("ES查询是否存在索引{}失败", index, e);
            return false;
        }
    }
}
根据环境做不同的索引缓存,这里主要是为了做动态索引
private List<String> findAllZeroIndex() {
    RBucket<String> allTopicCache = redissonClient.getBucket("allTopic" + envConfig.getCurrentEnv());
    String cacheStr = allTopicCache.get();
    if (StringUtils.isNotBlank(cacheStr)) {
        String[] split = cacheStr.split(",");
        return Arrays.asList(split);
    }
    List<String> indexList = new ArrayList<>();
    try {
        GetIndexRequest request = new GetIndexRequest("*");
        GetIndexResponse getIndexResponse = zeroClient.indices().get(request, RequestOptions.DEFAULT);
        String[] indices = getIndexResponse.getIndices();
        indexList = Arrays.asList(indices);
        allTopicCache.set(String.join(",", indexList), 24, TimeUnit.HOURS);
    } catch (IOException e) {
        log.error("查询所有索引数据", e);
    }
    return indexList;
}
/**
 * 批量新增日志
 */
public void bulkAddRequest(List<LogMsgDTO> msgList) {
    BulkRequest request = new BulkRequest();
    //等待批量请求作为执行的超时设置为2分钟
    request.timeout(TimeValue.timeValueMinutes(2));
    msgList.forEach(msg -> {
        request.add(new IndexRequest(msg.getTopic()).source(msg.getMsg(), XContentType.JSON));
    });
    zeroClient.bulkAsync(request, RequestOptions.DEFAULT, new ActionListener<BulkResponse>() {
        @Override
        public void onResponse(BulkResponse bulkResponse) {
            for (BulkItemResponse bulkItemResponse : bulkResponse) {
                boolean failed = bulkItemResponse.isFailed();
                if (failed) {
                    log.error("批量插入消息失败,详细信息={}", bulkItemResponse.getFailureMessage());
                }
                DocWriteResponse itemResponse = bulkItemResponse.getResponse();
                switch (bulkItemResponse.getOpType()) {
                    case INDEX:
                    case CREATE:
                        IndexResponse indexResponse = (IndexResponse) itemResponse;
                        handleAddDocSuccess(indexResponse);
                        break;
                    case UPDATE:
                        UpdateResponse updateResponse = (UpdateResponse) itemResponse;
                        break;
                    case DELETE:
                        DeleteResponse deleteResponse = (DeleteResponse) itemResponse;
                        break;
                    default:
                        break;
                }
            }
        }
        @Override
        public void onFailure(Exception e) {
            log.error("批量插入失败", e);
        }
    });
}
创建索引JSON
{
	"properties": {
		"userId": {
			"type": "keyword"
		},
		"userName": {
			"type": "text",
			"analyzer": "ik_max_word",
			"search_analyzer": "ik_smart"
		},
		"requestUrl": {
			"type": "text",
			"analyzer": "ik_max_word",
			"search_analyzer": "ik_smart"
		},
		"requestType": {
			"type": "keyword"
		},
		"logType": {
			"type": "keyword"
		},
		"requestMethod": {
			"type": "text",
			"analyzer": "ik_max_word",
			"search_analyzer": "ik_smart"
		},
		"env": {
			"type": "keyword"
		},
		"localIp": {
			"type": "keyword"
		},
		"requestIp": {
			"type": "keyword"
		},
		"triceId": {
			"type": "keyword"
		},
		"createTime": {
			"type": "date",
			"format": "yyyy-MM-dd HH:mm:ss.SSS"
		},
		"system": {
			"type": "keyword"
		},
		"threadName": {
			"type": "keyword"
		},
		"msg": {
			"type": "text",
			"analyzer": "ik_max_word",
			"search_analyzer": "ik_smart"
		}
	}
}

注意事项

  • 切记kafka的topic要提前创建好,不然打不出日志来
  • 数据处理服务的日志不抓,避免套娃
  • 注意ES转换日期时默认是0时区

日志收集第三版

设计方案

Filebeat+Kafka+数据处理服务+Elasticsearch+Kibana+Skywalking

依旧是基于第二版,收集日志的话采用Filebeat,相比Log42直接推送可以收集更多类型的日志,比如容器,中间件的日志,不局限于spring boot项目。然后引入了Skywalking做一个链路追踪的补充,其他部分保持不变

思考

第二版已经很好用了,为啥还要做第三版呢?折腾嗷,不停地折腾。其实个人而言,服务日志相对来说第二版已经完全没问题了,但是对于团队来说,单一的服务收集还是不够的,尽管我为某些项目定制了行为日志收集,场景依然是偏少的。于是引入了Filebeat,beat系列组件的引入主要是考虑到我自己部署的一些中间件日志想要进行管理,比如Seata,Mysql,Redis,Nacos之类的。对于这种中间件我都是上Linux敲命令查日志,没有集成起来,毕竟总不能说我把源码改了,把自己这套Log4j2的给嵌进去吧,那不合适还挺傻,所以Filebeat,给爷上。

Skywalking的接入也是链路追踪的实现方案之一吧,之所以选择这个也是因为有别的同事有弄过,现成的直接能用,对接上Log4j2就带上链路ID了,查日志也很方便

这几天和架构组的同事聊了聊,他们现在是基于K8s环境来做的日志收集,还是ELK的体系,实现和我的方案类似,用的收集工具不太一样。他们需要解决的问题更多,多地数据聚合,老项目的日志格式需要兼容,多种类型语言服务的日志处理等等。他们现在的方案已初具雏形,解决了部分问题,也能启动了。后续更新的话,我会朝该方向更新,兼容更多的场景,解决更多的问题。

代码实现

Skywalking配置

 <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-log4j-2.x</artifactId>
    <version>8.7.0</version>
</dependency>

探针配置,以下是jvm参数 ${skywalkingIp}是容器配置环境用的自定义参数
-javaagent:/usr/local/app/skywalking_agent_zy/skywalking-agent.jar -Dskywalking.agent.service_name=${appName} -Dskywalking.collector.backend_service=${skywalkingIp}:${skywalkingPort} -Dskywalking.plugin.toolkit.log.grpc.reporter.server_host=${skywalkingIp}

Filebeat配置

#指定多个输入源
filebeat.inputs:
  #选择输入类型,这里选日志文件,因为咱们收集的是log4j2产生的log文件
  - type: log
    enabled: true
    #扫描路径下文件,可以写多个路径
    paths:
      - /data/myLog/*.log
    #include_lines,exclude_lines里面用正则,排除某一行读取的数据
    #exclude_lines: [ '.*\|dev\|.*' ]
    #与您希望 Filebeat 忽略的文件匹配的正则表达式列表。默认情况下不排除任何文件。
    exclude_files: [ '.bak$','-error.log$' ]
    recursive_glob:
      enabled: false
    #每个收集器在获取文件时使用的缓冲区大小(以字节为单位)。默认值为 16384。
    harvester_buffer_size: 16384
    #单个日志消息可以拥有的最大字节数。之后的所有字节 max_bytes都被丢弃而不发送。此设置对于可能变得很大的多行日志消息特别有用。默认值为 10MB (10485760)。
    #这里因为我log4j2.xml里面配置的以100MB滚动,所有这里调的大点
    max_bytes: 209715200
    #Filebeat 将忽略在指定时间跨度之前修改的所有文件.您可以使用时间字符串,例如 2h(2 小时)和 5m(5 分钟)。默认值为 0,即禁用该设置。
    #这里设置为2h,即排除掉容器修改时间为2h之前以.log为结尾的日志,防止传输无用历史数据,但是今天的日志文件还是包含在内
    ignore_older: 2h
    #如果一个文件在某个时间段内没有发生过更新,则关闭监控的文件handle。默认5m。
    close_inactive: 5m
    #如果此选项设置为true,Filebeat 会在每个文件的末尾而不是开头开始读取新文件。当此选项与日志轮换结合使用时,可能会跳过新文件中的第一个日志条目。默认设置为false。
    tail_files: false
    #定义要使用的聚合方法。默认值为pattern. 另一个选项是count让您聚合恒定数量的行。
    multiline.type: pattern
    #特殊正则,详见官网https://www.elastic.co/guide/en/beats/filebeat/7.9/regexp-support.html
    multiline.pattern: '(^[a-z]+.*Exception)|(^[[:space:]]+(at|\.{3}\b|^Caused by:))'
    #false为正则表达式取反,即不匹配的情况
    multiline.negate: false
    #after或before,这里after的意思是将合并后的数据,追加到不匹配的上一行。也就是说怎么也得是两行合并。
    multiline.match: after

filebeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false
  #reload.period: 10s
#filebeat收集信息后是以JSON的方式传输,并且会附带很多信息,比如时间。这里就是减掉这些无用的字段。
processors:
  - drop_fields:
      fields: [ "host", "agent", "log", "input", "ecs" ]
#输出到Kafka
output.kafka:
  hosts: [ "192.168.158.162:9092","192.168.158.163:9092","192.168.158.164:9092" ]
  topics:
    #消息中包含这个字符串,选择这个topic
    - topic: "filebeat-dev"
      when.contains:
        message: "|dev|"
    - topic: "filebeat-uat"
      when.contains:
        message: "|uat|"
  #Kafka 输出代理事件分区策略。必须是random、 round_robin或之一hash。默认情况下使用hash分区程序。
  #reachable_only: true 设置为true时事件将仅发布到Kafka可用分区
  partition.round_robin:
    reachable_only: true
  #单个 Kafka 请求中要批量处理的最大事件数。默认值为 2048。
  bulk_max_size: 2048
  #发送批量 Kafka 请求之前的等待时间。0 是没有延迟。默认值为 0。
  bulk_flush_frequency: 0
  #在超时之前等待来自 Kafka 代理的响应的秒数。默认值为 30(秒)。
  timeout: 30
  #代理将等待所需 ACK 数的最长时间。默认值为 10 秒。
  broker_timeout: 10
  #每个 Kafka 代理在输出管道中缓冲的消息数。默认值为 256。
  channel_buffer_size: 256
  #活动网络连接的保持活动期。如果为 0,则禁用保活。默认值为 0 秒。
  keep_alive: 0
  #设置输出压缩编解码器。必须是none,snappy和lz4之一gzip。默认值为gzip.
  compression: gzip
  #设置 gzip 使用的压缩级别。将此值设置为 0 将禁用压缩。压缩级别必须在 1(最佳速度)到 9(最佳压缩)的范围内。
  #增加压缩级别会降低网络使用率,但会增加 CPU 使用率。 默认值为 4。
  compression_level: 4
  #JSON 编码消息的最大允许大小。更大的消息将被丢弃。默认值为 1000000(字节)。此值应等于或小于经纪人的message.max.bytes.
  max_message_bytes: 10485760
  #代理要求的 ACK 可靠性级别。0=无响应,1=等待本地提交,-1=等待所有副本提交。默认值为 1。
  #注意:如果设置为 0,Kafka 不会返回任何 ACK。消息可能会因错误而静默丢失。
  required_acks: 1

收集Seata日志

filebeat.yml中增加fields.app:seata,seata这个日志可以修改源码中的log.xml文件,但是我偷懒了,直接整行用msg算了

@KafkaListener(topics = {"filebeat-#{systemProperties['env']}"}, containerFactory = "manualFactory")
public void kafkaUatHandler(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
    //尽量保持和生产者的批量大小一致,避免频繁扩容
    List<LogMsgDTO> logMsgList = new ArrayList<>(2048);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
    records.forEach(record -> {
        String topic = record.topic() + "-" + formatter.format(LocalDate.now());
        String value = record.value();
        JSONObject beatRes = JSON.parseObject(value);
        //获取中间件名称
        String app = beatRes.getJSONObject("fields").getString("app");
        //直接获取filebeat收集时间即可
        String date = beatRes.getString("@timestamp");
        //seata或者其他中间件的一行日志
        String message = beatRes.getString("message");
        boolean existIndex = esService.existIndex(topic + "-alias1");
        if (!existIndex) {
            middlewareEsService.createIndex(topic + "-alias1", topic, false);
        }
        if (StringUtils.isNotBlank(message)) {
            MiddlewareDTO data = new MiddlewareDTO(app, date, message);
            logMsgList.add(new LogMsgDTO(topic, JSON.toJSONString(data)));
        } else {
            log.warn("异常数据无法获取message字段=={}", value);
        }
    });
    if (!CollectionUtils.isEmpty(logMsgList)) {
        esService.bulkAddRequest(logMsgList);
    }
    ack.acknowledge();
}

写在最后

完整代码没发也没传GIT,因为有些公司的东西,我也懒得去挨个删了,于是就发了一些关键部分的代码。如果有感兴趣或者想要和我讨论的,可以站内信或者评论一下。

更希望大家能够帮我看看我这套方案还有没有什么地方可以做的更好,求指教。

该文会持续更新,原文是在我的有道云笔记里,有修改的话我会同步过来。距离上一篇已经过了一个月了,淦,最近是真的忙。

今天是2023/09/13,回看真是感慨良多,自动第三版之后也算是没有更新了,很遗憾。主要是贫瘠的业务场景属实没有发挥空间,而且博主的日常工作也比较忙碌,交给其他同事打理,但很明显只是维护了,作为一个记录几十人开发团队的小系统也算是绰绰有余了。架构组的日志管理系统,当时底层吹的逆天,结果最后还是采取了和我类似的架构,哈哈,在我之上去解决了,多地聚合、备份之类的问题,比较有意思,可惜我没参与上,太忙了。如果对这个东西感兴趣的话,可以私信我问问题,我可以摘除敏感信息后,提供源码。

更新记录

2022/07/27

  1. 更新filebeat多行合并配置,用于处理Java异常信息
  2. ES的mapping字段类型优化,主要是ik分词器使用官方推荐的写法

2022/08/18

  1. 更新seata日志收集代码

2023/09/13

  1. 更新ElasticSearch的性能优化手段