SpringCloud中服务集成PlumeLog日志系统

目录

一、plumeLog是什么

二、ES搭建

2.1、下载ES安装包安装

2.2、修改elasticsearch.yml配置

2.3、修改系统相关配置

2.4、启动ES服务

[2.5、使用 Systemd 管理ES服务(开机自启)](#2.5、使用 Systemd 管理ES服务(开机自启))

三、plumeLog-server搭建

3.1、下载安装包安装

3.2、修改application.properties配置

3.3、启动服务

四、springboot服务集成plumelog

4.1、项目引入pom.xml依赖

4.2、项目logback-spring.xml配置

4.3、增加TraceId过滤器(Http请求线程打入TraceId)

4.4、全局异常处理器改造

4.5、新增TraceResponseAdvice,返回对象Resp中设置traceId

4.6、其他后台线程设置traceId

五、请求接口验证

5.1、正常响应请求

5.2、异常响应请求

5.3、plume日志查看

[六、扩展: 雪花算法中workerId怎么取](#六、扩展: 雪花算法中workerId怎么取)


在我们实际的项目开发过程中,服务日志从收集、索引及查询也是非常重要的一环,本篇给大家介绍下一款非常优秀的日志收集、处理框架plumeLog

一、plumeLog是什么

参考官网:README

1、plume是无代码入侵的分布式日志系统,基于log4j、log4j2、logback搜集日志,设置链路ID,2、方便查询关联日志

3、基于elasticsearch作为查询引擎

4、高吞吐,查询效率高

5、全程不占应用程序本地磁盘空间,免维护;对于项目透明,不影响项目本身运行

6、无需修改老项目,引入直接使用,支持dubbo,支持springcloud

二、ES搭建

从plumeLog的架构图上我们得知,日志的底层存储是依赖ES的,我们先最底层开始搭建起,先搭建ES,笔者采用的是elasticsearch7.x的版本。

2.1、下载ES安装包安装

下载安装包(若已上传至服务器可跳过)

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.1-linux-x86_64.tar.gz

解压文件

tar -zxvf elasticsearch-7.10.1-linux-x86_64.tar.gz

移动到 /usr/local/ 目录并重命名

mv elasticsearch-7.10.1 /usr/local/elasticsearch

创建数据目录、日志目录

mkdir -p /data/es/data

mkdir -p /data/es/log

#创建es用户

groupadd elastic

useradd -g elastic elastic

将es目录所属主用户赋予给elastic用户

chown -R elastic:elastic /usr/local/elasticsearch

chown -R elastic:elastic /data/es

2.2、修改elasticsearch.yml配置

bash 复制代码
cluster.name: my-application    # 集群名称
node.name: node-1                # 节点名称
path.data: /var/data/elasticsearch   # 数据存放路径
path.logs: /var/log/elasticsearch    # 日志存放路径
network.host: 0.0.0.0            # 允许所有IP访问,生产环境建议设置具体IP
http.port: 9200                  # 端口号
discovery.seed_hosts: ["127.0.0.1"]   # 集群自动发现节点
discovery.type: single-node #单节点加上这项

#cluster.initial_master_nodes: ["node-1"]  如果是多节点则加上这项,指定默认初始化的主节点

如果有内存限制,可以修改 JVM 配置文件 /usr/local/elasticsearch/config/jvm.options,一般建议设为物理内存的一半,且不超过 32GB

bash 复制代码
# 修改 JVM 堆内存大小,根据你服务器内存情况调整(单位:m 或 g)
-Xms1g
-Xmx1g

2.3、修改系统相关配置

1、修改文件描述符和用户进程限制,编辑 /etc/security/limits.conf 文件,在末尾添加:

bash 复制代码
* soft nofile 65536
* hard nofile 65536
* soft nproc 4096
* hard nproc 4096

2、修改虚拟内存区域最大数量,编辑 /etc/sysctl.conf 文件,在末尾添加:

bash 复制代码
vm.max_map_count=262144

完成后执行 sysctl -p 使配置立即生效

2.4、启动ES服务

切换到 elastic 用户,启动服务

bash 复制代码
su elastic
cd /usr/local/elasticsearch/bin
./elasticsearch -d

2.5、使用 Systemd 管理ES服务(开机自启)

创建服务文件 /etc/systemd/system/elasticsearch.service,这样可以用 systemctl 来管理,并支持开机自启

bash 复制代码
[Unit]
Description=Elasticsearch

[Service]
User=elastic
ExecStart=/usr/local/elasticsearch/bin/elasticsearch
Restart=always
LimitNOFILE=65535
LimitNPROC=4096

[Install]
WantedBy=multi-user.target

保存后执行以下命令,使之生效:

systemctl daemon-reload

systemctl enable elasticsearch

systemctl start elasticsearch

在服务器本地执行以下命令测试:

curl http://localhost:9200

bash 复制代码
{
  "name" : "node-1",
  "cluster_name" : "my-application",
  "cluster_uuid" : "_na_",
  "version" : {
    "number" : "7.10.1",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "1c34507e66d7db1211f66f3513706fdf548736aa",
    "build_date" : "2020-12-05T01:00:33.671820Z",
    "build_snapshot" : false,
    "lucene_version" : "8.7.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

至此ES搭建完成 !

三、plumeLog-server搭建

plumeLog-server的搭建相对笔记简单,需要注意的是连接ES的地址要确保正确,我们采用最常见的redis队列模式,当然你也可以采用kafka作消息中间件。plumeLog-server笔者使用的是最新的3.5.3的版本。

3.1、下载安装包安装

下载地址:Plumelog

unzip plume-log-server-3.5.zip

mv plume-log-server-3.5 /usr/local #移动目录

3.2、修改application.properties配置

bash 复制代码
plumelog.model=redis
plumelog.queue.redis.redisHost=localhost:6379
plumelog.queue.redis.redisPassWord=123456
plumelog.queue.redis.redisDb=11

#管理端redis地址 ,集群用逗号隔开,不配置将和队列公用
plumelog.redis.redisHost=127.0.0.1:6379
plumelog.redis.redisPassWord=123456
plumelog.redis.redisDb=11

elasticsearch相关配置,Hosts支持携带协议,如:http、https
plumelog.es.esHosts=localhost:9200
plumelog.es.shards=3
plumelog.es.replicas=1
plumelog.es.refresh.interval=30s
#日志索引建立方式day表示按天、hour表示按照小时
plumelog.es.indexType.model=day
plumelog.es.maxShards=100000
#ES设置密码,启用下面配置
plumelog.es.userName=elastic
plumelog.es.passWord=elastic

3.3、启动服务

bash 复制代码
cd /usr/local/plume-log-server-3.5
./startup.sh

查看plumeLog服务日志:

四、springboot服务集成plumelog

4.1、项目引入pom.xml依赖

XML 复制代码
     <!--    引入plumeLog -->
        <dependency>
            <groupId>com.plumelog</groupId>
            <artifactId>plumelog-logback</artifactId>
            <version>3.5.3</version>
        </dependency>
        <dependency>
            <groupId>com.plumelog</groupId>
            <artifactId>plumelog-trace</artifactId>
            <version>3.5.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

4.2、项目logback-spring.xml配置

XML 复制代码
  <appender name="plumeLog" class="com.plumelog.logback.appender.RedisAppender">
    <appName>tmccloud-merchant</appName>
    <redisHost>192.168.31.111:6379</redisHost>
    <redisAuth>123456</redisAuth>
    <redisDb>11</redisDb>
    <env>dev</env>
  </appender>

 
 
    <root level="INFO">
      <appender-ref ref="CONSOLE" />
      <appender-ref ref="ASYNC"/>
      <appender-ref ref="errorLog" />
       <!-- 加入plumeLog-->
      <appender-ref ref="plumeLog"/>
    </root>
 

4.3、增加TraceId过滤器(Http请求线程打入TraceId)

PlumeLogFilterConfig.java

java 复制代码
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PlumeLogFilterConfig {
    @Bean
    public FilterRegistrationBean filterRegistrationBean1() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(myTraceIdFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setOrder(Integer.MIN_VALUE);
        return filterRegistrationBean;
    }

    @Bean
    public Filter myTraceIdFilter() {
        return new MyTraceIdFilter();
    }
}

MyTraceIdFilter.java

java 复制代码
import cn.hutool.core.util.IdUtil;
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.base.worker.WorkerIdUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class MyTraceIdFilter implements Filter {


    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            String traceId = request.getParameter("traceId");
            if (traceId != null && !"".equals(traceId)) {
                TraceId.logTraceID.set(traceId);
            } else {
                //TraceId.set();
                // 给当前线程打入traceId
                long workerId = WorkerIdUtil.getWorkerIdByIp();
                String id = IdUtil.getSnowflake(workerId, 0).nextIdStr();
                TraceId.logTraceID.set(id);
            }
        } finally {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    public void destroy() {
    }
}

4.4、全局异常处理器改造

将Resp中设置TraceId, 方便搜索报错日志信息。

java 复制代码
 


import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.base.core.Resp;
import com.tingcream.tmccloud.base.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.resource.NoResourceFoundException;


import java.util.List;

/**
 * 全局异常处理器
 */
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * JSR303 异常
     * @param e
     * @param request
     * @return
     */
    @ExceptionHandler({BindException.class})
    public Resp<?> handleBindException(BindException e, HttpServletRequest request) {
        log.error(e.getMessage());//记录完整错误信息
        List<ObjectError> errors = e.getBindingResult().getAllErrors();
        ObjectError err= errors.get(0);
        String msg = err.getDefaultMessage();
        Resp res=  Resp.error(msg);
        res.setTraceId(TraceId.logTraceID.get());
        return res ;
    }
    /**
     * JSR303 异常
     * @param e
     * @param request
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Resp<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
        log.error(e.getMessage());//记录完整错误信息
        List<ObjectError> errors = e.getBindingResult().getAllErrors();
        ObjectError err= errors.get(0);
        String msg = err.getDefaultMessage();

        Resp res=  Resp.error(msg);
        res.setTraceId(TraceId.logTraceID.get());
        return res ;
    }
    /**
     * JSR303 异常
     * @param e
     * @param request
     * @return
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Resp<?> handleConstraintViolationExceptionException(ConstraintViolationException e, HttpServletRequest request) {
        log.error(e.getMessage());
        ConstraintViolation c  = (ConstraintViolation)e.getConstraintViolations().toArray()[0];
        String msg =c.getMessage();
        Resp res=  Resp.error(msg);
        res.setTraceId( TraceId.logTraceID.get());
        return res ;
    }


    /**
     * BusinessException 自定义业务异常处理
     * @param e
     * @param request
     * @return
     */
    @ExceptionHandler({BusinessException.class})
    public Resp<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
        //返回自定义的状态code和msg
        log.error(e.getMsg());
        Resp res= new Resp<>(e.getCode(), e.getMsg(), e.getData());
        res.setTraceId( TraceId.logTraceID.get());
        return res ;
    }

    /**
     * Exception 异常
     * @param e
     * @param request
     * @return
     */
    @ExceptionHandler({Exception.class})
    public Resp<?> handleException(Exception e, HttpServletRequest request) {
        log.error(e.getMessage(),e);
        Resp res;
        if(e instanceof HttpRequestMethodNotSupportedException){
            res= Resp.error("客户端http请求方式有误,请检查!");
        }else{
            res= Resp.error("抱歉,系统异常!");
        }
        res.setTraceId( TraceId.logTraceID.get());
        return res ;
    }


    @ExceptionHandler(NoResourceFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<?> handleNoResourceFound(NoResourceFoundException e) {
        // 如果是 favicon.ico 请求,直接返回空,不记录日志
        if (e.getMessage() != null && e.getMessage().contains("favicon.ico")) {
            // 可以返回一个空的 404 响应,或者直接返回 null
            return ResponseEntity.notFound().build();
        }
        // 其他资源找不到的情况,根据需要处理
        log.warn("资源未找到: {}", e.getMessage());
        return ResponseEntity.notFound().build();
    }
}

Resp响应类

java 复制代码
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Resp<T> {

    public static final int SUCCESS=200;
    public static final int FAILURE=500;
    public static final int ERROR=9999;

    /**
     * 返回状态码(200 操作成功 500操作失败  9999系统异常...)
     */
    private Integer code ;

    /**
     * 返回消息
     */
    private String msg ;

    /**
     * 日志追踪码Id
     */
    private String traceId;


    /**
     * 返回数据
     */
    private T data;

    public Resp(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Resp(Integer code, String msg,T data) {
        this.code = code;
        this.msg = msg;
        this.data=data;
    }

    public static <T> Resp<T> success(){
        return new Resp(SUCCESS,"操作成功") ;
    }
    public static <T> Resp<T> successMsg(String msg){
        return new Resp(SUCCESS,msg) ;
    }
    public static <T> Resp<T> success(T data){
        return new Resp(SUCCESS,"操作成功",data) ;
    }


    public static <T> Resp<T> failure(String msg){
        return new Resp(FAILURE,msg) ;
    }

    public static <T> Resp<T> error(String msg){
        return new Resp(ERROR,msg) ;
    }

}

4.5、新增TraceResponseAdvice,返回对象Resp中设置traceId

java 复制代码
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.base.core.Resp;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice
public class TraceResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 只处理 Resp 类型的返回值
        return returnType.getParameterType() == Resp.class;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        if (body instanceof Resp) {
            Resp<?> resp = (Resp<?>) body;
            // 从 Plumelog 上下文中获取当前请求的 traceId
            String traceId = TraceId.logTraceID.get();
            resp.setTraceId(traceId);
        }
        return body;
    }
}

4.6、其他后台线程设置traceId

如xxl-job的定时任务执行,并非是用户http请求行为,而是后台线程执行,这时我们需要手动打入traceId才行。

java 复制代码
import cn.hutool.core.util.IdUtil;
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.base.worker.WorkerIdUtil;
import org.slf4j.MDC;
import java.util.UUID;

/**
 * 手动打入traceId  例如xxl-job定时任务中的后台线程
 */
public class TraceHelper {

    // MDC 中的 Key,Plumelog 默认会识别
    private static final String TRACE_ID_KEY = "traceId";

    public static void setTraceId() {
        long workerId = WorkerIdUtil.getWorkerIdByIp();
        String traceId = IdUtil.getSnowflake(workerId, 0).nextIdStr();

        TraceId.logTraceID.set(traceId); // 设置 Plumelog 上下文
        MDC.put(TRACE_ID_KEY, traceId);   // 设置 MDC,方便普通日志打印
    }

    public static void clearTraceId() {
        TraceId.logTraceID.remove();
        MDC.remove(TRACE_ID_KEY);
    }
}

五、请求接口查看日志

发现无论是正常响应的请求还是异常请求,返回结果中都给出了traceId 。开发人员只需要拿着这个id就可以精准地搜索到相关的日志

5.1、正常响应请求

5.2、异常响应请求

5.3、plume日志查看

可根据应用名称、环境、追踪码等条件查询, 当然我们直接输入追踪码查询才是最方便直接的!

六、扩展: 雪花算法中workerId怎么取

请注意 我们项目中生成traceId 使用的是雪花算法,雪花算法中最重要的概念是workerId、datacenterId。那么我们该怎么得到服务的workerId呢?

首先指出直接在nacos配置死服务的workerId是不对的, 这样做服务的所有实例都是相同的workerId了,不具有区分性。最好的做法是根据服务所在的物理网络环境生成workerId ,我们也可根据服务所在机器的ip、主机名 或docker容器id及容器名称等 得到workerId。

以下是笔者生成workerId的简单实现 (根据机器ip)

java 复制代码
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;

public class WorkerIdUtil {

    /**
     * 根据IP地址生成workerId
     * 策略:取IPv4地址的最后两段,计算哈希后映射到0-31范围
     */
    public static long getWorkerIdByIp() {
        try {
            String ip = getLocalIpAddress();
            // 例如:192.168.31.1 -> 取最后两段 "31.1"
            String[] ipParts = ip.split("\\.");
            if (ipParts.length >= 4) {
                String lastTwo = ipParts[2] + "." + ipParts[3];
                // 计算哈希并映射到0-31范围
                return Math.abs(lastTwo.hashCode()) % 32;
            }
            // 备用方案:使用完整IP的哈希
            return Math.abs(ip.hashCode()) % 32;
        } catch (Exception e) {
            // 降级方案:返回默认值
            return 1L;
        }
    }

    /**
     * 获取本机IPv4地址(优先取eth0网卡)
     */
    private static String getLocalIpAddress() throws Exception {
        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface ni = interfaces.nextElement();
            // 跳过回环地址和未启用的网卡
            if (ni.isLoopback() || !ni.isUp()) {
                continue;
            }
            Enumeration<InetAddress> addresses = ni.getInetAddresses();
            while (addresses.hasMoreElements()) {
                InetAddress addr = addresses.nextElement();
                if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress()) {
                    return addr.getHostAddress();
                }
            }
        }
        // 降级:获取本地主机地址
        return InetAddress.getLocalHost().getHostAddress();
    }

}
相关推荐
程序猿乐锅1 小时前
【苍穹外卖|Day01】项目初识:从多模块结构到 OpenAPI 接口文档踩坑
java·spring·maven·mybatis
linweidong1 小时前
Java 后端开发面试 50 个高频易混淆知识点详解
java·spring boot·spring·spring cloud·面试·mybatis·spring事务
Resky08181 小时前
什么是 Spring IOC:倒过来让容器帮你 new,而不是你到处 new
java·spring
AutumnWind04201 小时前
【JDK动态代理源码梳理】
java·后端·spring
砍材农夫13 小时前
物联网实战:Spring Boot MQTT | MQTT 设备模拟器演示(附源码)
java·spring boot·后端·物联网·spring·netty
EAIReport13 小时前
Spring AI 详解:Java 开发者快速落地 AI 应用
java·人工智能·spring
爱吃牛肉的大老虎15 小时前
Spring中用到的设计模式
java·spring·设计模式
Stick_ZYZ15 小时前
从“能调用工具”到“能稳定执行任务”:Agent 工程化的下一步
java·人工智能·后端·spring·ai