微服务日志输出如何处理?

Api日志

为了方便日志跟踪,api日志一般会要求显示请求路径,方法类型,执行时间,请求的入参,请求头,以及请求响应。如果是微服务之间的调用,需要设置全局的请求id用于标识整个链路请求。基于控制层的拦截器实现,定义LogFilter实现日志的输出。微服务之间请求的全局id通过header传递,使用HttpServlet的装饰类进行扩展header的设置。源码如下:

public class LogFilter implements Filter {

    private static final Set<String> LOCAL_HOST_SET = Sets.newHashSet(LOCAL_HOST_IP, LOCAL_HOST);
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        if (this.isLocalhostRequest(request.getLocalAddr())
                || this.isFile(request.getContentType())) {

            chain.doFilter(request, response);
            return;
        }

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        RequestLogWrapper requestLogWrapper = new RequestLogWrapper(httpServletRequest);
        ResponseLogWrapper responseLogWrapper = new ResponseLogWrapper(httpServletResponse);
        // 在header中定义全局的请求id
        String requestId;
        if (StringUtils.isBlank(requestId = requestLogWrapper.getHeader(REQUEST_ID))) {

            requestId = UUID.randomUUID().toString();
            // 重新设置requestId
            requestLogWrapper.setHeader(REQUEST_ID, requestId);
        }

        // 定义服务执行开始结束时间
        LocalDateTime startAt = DateUtils.now();
        chain.doFilter(requestLogWrapper, responseLogWrapper);
        LocalDateTime finishAt = DateUtils.now();

        HttpLog httpLog = HttpLogUtil.obtainResponseLog(requestLogWrapper, responseLogWrapper, startAt, finishAt, requestId);
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(responseLogWrapper.getResponseData());
        outputStream.flush();
        outputStream.close();

        // 打印response
        log.info("----------------------http log -------------------------");
        String requestParams;
        String headers;
        log.info("requestId: [{}],methodName: [{}],url: [{}],startAt: [{}],finishAt: [{}],duration: [{}ms],status: [{}],headers:[{}],request: [{}],response: [{}]",
                httpLog.getRequestId(),
                httpLog.getMethodName(),
                httpLog.getUrl(),
                DateUtils.formatDate(httpLog.getStartAt(), DateUtils.DATE_TIME_MILE),
                DateUtils.formatDate(httpLog.getFinishAt(), DateUtils.DATE_TIME_MILE),
                httpLog.getDuration(),
                httpLog.getStatus(),
                StringUtils.isNotBlank(headers = httpLog.getHeaders()) ? HttpLogUtil.subStringIfRequired(headers) : StringUtils.EMPTY,
                StringUtils.isNotBlank(requestParams = httpLog.getRequest()) ? HttpLogUtil.subStringIfRequired(requestParams) : StringUtils.EMPTY,
                HttpLogUtil.subStringIfRequired(httpLog.getResponse()));
    }

    private boolean isLocalhostRequest(String localAddr) {

        for (String localHost : LOCAL_HOST_SET) {
            if (localHost.equalsIgnoreCase(localAddr)) {
                return true;
            }
        }
        return false;
    }

    private boolean isFile(String contentType){
        if(StringUtils.isNotEmpty(contentType)&&contentType.contains(FILE_CONTENT_TYPE)){
            return true;
        }
        return false;
    }

    @Override
    public void destroy() {
    }
}



public class RequestLogWrapper extends HttpServletRequestWrapper {

    /**
     * 所有header参数集合,在HttpServletRequest内部,header名称已经全部给转换小写
     */
    private final Map<String, String> headerMap;
    /**
     * 请求body
     */
    private final byte[] body;

    public RequestLogWrapper(HttpServletRequest request) {
        super(request);
        this.body = this.initBody(request);
        this.headerMap = this.initHeaderMap(request);
    }

    private byte[] initBody(HttpServletRequest request) {

        String bodyString;
        return StringUtils.isNotBlank(bodyString = this.getRequestBodyString(request))
                ? bodyString.getBytes(Charset.defaultCharset()) : new byte[]{};
    }

    private Map<String, String> initHeaderMap(HttpServletRequest request) {

        Map<String, String> headerMap = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            headerMap.put(headerName, request.getHeader(headerName));
        }
        return headerMap;
    }

    public void setHeader(String header, String value) {

        if (StringUtils.isBlank(header) || StringUtils.isBlank(value)) {
            return;
        }
        this.headerMap.put(header, value);
    }

    @Override
    public String getHeader(String name) {
        return StringUtils.isNotBlank(name) ? this.getHeaderIgnoreCase(name) : null;
    }

    /**
     * header名称已经全部给转换小写,现根据源格式获取,如果获取不到再转换为小写格式
     */
    private String getHeaderIgnoreCase(String name) {
        String value;
        if (StringUtils.isNotBlank(value = this.headerMap.get(name))) {
            return value;
        }
        return this.headerMap.get(name.toLowerCase());
    }

    /**
     * 封装request id 的header
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        return Collections.enumeration(this.headerMap.keySet());
    }

    private String getRequestBodyString(HttpServletRequest request) {

        StringBuffer sb = new StringBuffer();
        BufferedReader bufferedReader = null;
        try {

            ServletInputStream inputStream = request.getInputStream();
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String lineStr;
            while (StringUtils.isNotBlank(lineStr = bufferedReader.readLine())) {
                sb.append(lineStr);
            }
        } catch (IOException e) {

            log.error("log filter  parse request exception!", e);
            throw new RuntimeException(e);
        } finally {
            if (Objects.nonNull(bufferedReader)) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {

                    log.error("log filter  close inputStream exception!", e);
                    throw new RuntimeException(e);
                }
            }
        }
        return sb.toString();
    }

    /**
     * 获取request 的json的请求参数
     */
    public String getBodyJson() {
        return ArrayUtils.isNotEmpty(body) ? new String(body, Charset.defaultCharset()) : StringUtils.EMPTY;
    }

    @Override
    public ServletInputStream getInputStream() {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

 

Feign日志

在springcloud框架中,通常使用feign实现服务的相互之间的调用。Feign输出日志只会答应出debugger级别的日志,并且日志的类型分为如下四类:

  • NONE:不打印日志
  • BASIC:只打印基本信息,包括请求方法、请求地址、响应状态码、请求时长
  • HEADERS:在 BASIC 基础信息的基础之上,增加请求头、响应头
  • FULL:打印完整信息,包括请求和响应的所有信息。

自定义Slf4jFeignLogger实现info级别的日志输出,并且根据日志类别打印不同类型日志。源码如下:

public class Slf4jFeignLogger extends feign.Logger {

    private final Logger logger;

    public Slf4jFeignLogger(Class<?> clazz) {
        this(LoggerFactory.getLogger(clazz));
    }

    public Slf4jFeignLogger(String name) {
        this(LoggerFactory.getLogger(name));
    }

    public Slf4jFeignLogger() {
        this(feign.Logger.class);
    }

    public Slf4jFeignLogger(Logger logger) {
        this.logger = logger;
    }


    @Override
    protected void log(String configKey, String format, Object... args) {

        if (logger.isInfoEnabled()) {

            logger.info(String.format(methodTag(configKey), args) + format);
        }
    }

    @Override
    protected void logRequest(String configKey, Level logLevel, Request request) {

        Map<String, Collection<String>> headers = request.headers();
        HttpLog httpLog = HttpLog.builder()
                .headers(MapUtils.isNotEmpty(headers) ? JSON.toJSONString(headers) : StringUtils.EMPTY)
                .methodName(request.httpMethod().name())
                .request(this.getBodyText(request))
                .url(request.url())
                .startAt(DateUtils.now())
                .build();
        HttpLogHolder.set(httpLog);
    }

    private String getBodyText(Request request) {

        byte[] body;
        if (ArrayUtils.isNotEmpty(body = request.body())) {

            Charset charset;
            return Objects.nonNull(charset = request.charset()) ? new String(body, charset) : null;
        }
        return null;
    }


    @Override
    protected Response logAndRebufferResponse(String configKey,
                                              Level logLevel,
                                              Response response,
                                              long elapsedTime) throws IOException {

        HttpLog httpLog = HttpLogHolder.getAndRelease();
        if (Objects.nonNull(httpLog)) {

            httpLog.setFinishAt(DateUtils.now());
            httpLog.setDuration(httpLog.getFinishAt().toInstant(ZoneOffset.UTC).toEpochMilli() - httpLog.getStartAt().toInstant(ZoneOffset.UTC).toEpochMilli());
            httpLog.setStatus(response.status());

            String msg;
            switch (logLevel) {

                case FULL:

                    String bodyData;
                    if (StringUtils.isNotEmpty(bodyData = IOUtils.toString(response.body().asInputStream(), StandardCharsets.UTF_8.name()))) {

                        // 从response中获取数据
                        httpLog.setResponse(bodyData);
                        response = response.toBuilder().body(bodyData, StandardCharsets.UTF_8).build();
                    }
                    msg = String.format("url: [%s],methodName: [%s],startAt: [%s],finishAt: [%s],duration: [%sms],status: [%s],headers:[%s],request: [%s],response: [%s]"
                            , httpLog.getUrl(), httpLog.getMethodName(),
                            getMills(httpLog.getStartAt()), getMills(httpLog.getFinishAt()), httpLog.getDuration(), httpLog.getStatus(),
                            HttpLogUtil.subStringIfRequired(httpLog.getHeaders()), HttpLogUtil.subStringIfRequired(httpLog.getRequest()), HttpLogUtil.subStringIfRequired(httpLog.getResponse()));

                    break;
                default:
                    msg = String.format("url: [%s],methodName: [%s],startAt: [%s],finishAt: [%s],duration: [%sms],status: [%s],headers:[%s]"
                            , httpLog.getUrl(), httpLog.getMethodName(),
                            getMills(httpLog.getStartAt()), getMills(httpLog.getFinishAt()), httpLog.getDuration(), httpLog.getStatus(),
                            HttpLogUtil.subStringIfRequired(httpLog.getHeaders()));
                    break;
            }
            log(configKey, "----------------- feign log -----------------");
            log(configKey, msg);
        }
        return response;
    }
}

 

日志收集

随着微服务数量的增加,对于日志的收集通常采用ELK方式(ELK日志采集分析)进行统一处理。

 

<think>好的,我现在需要回答用户关于Go语言的工具、框架和最佳实践的问题,包括依赖管理工具、测试框架、部署方法、日志处理微服务框架。首先,我应该参考用户提供的引用内容,看看有没有相关信息可以整合。 用户给出的引用中,引用[1]提到了Docker和Docker Compose用于部署Go应用,确保环境一致性,这应该和部署方法相关。引用[2]提到了性能优化的多个方面,比如代码审查、性能测试、负载均衡、缓存机制等,这些可能在日志处理微服务框架的最佳实践中有所涉及。引用[3]虽然主要讲Python的Web框架,但可能可以对比说明Go的框架特点。 接下来,我需要分部分整理每个主题的信息: 1. 依赖管理工具:Go Modules是官方推荐的,需要说明基本用法,比如初始化、添加依赖等。 2. 测试框架:Go内置的testing包,可能需要介绍表格驱动测试、覆盖率工具,以及第三方库如testify。 3. 部署方法:Docker容器化部署,Docker Compose编排,以及可能的云服务部署如Kubernetes,这些在引用[1]中有提到。 4. 日志处理最佳实践:结构化日志、使用库如logrus或zap,结合ELK栈或云服务监控,引用[2]中的资源监控和缓存机制可能相关。 5. 微服务框架:比如GoKit、GoMicro,需要说明特点,服务发现、负载均衡等,引用[1]中的微服务编排和引用[2]的负载均衡和缓存机制可以结合。 然后,每个部分需要给出具体的例子或代码片段,比如Dockerfile示例,测试代码示例,日志配置示例等。同时,要确保使用正确的LaTeX格式,比如行内公式用$...$,独立公式用$$...$$,但可能这部分在技术回答中用得不多,主要注意代码块和结构。 还需要生成相关问题,参考用户示例中的结构,提出与每个部分相关的深入问题,比如依赖管理的版本控制、测试框架的Mocking工具、部署中的蓝绿部署策略、日志性能优化、微服务框架的选型比较等。 最后,检查引用标记是否正确添加,引用[1]和[2]中的相关内容需要自然融入回答中,比如部署方法部分引用Docker和监控工具集成,性能优化部分引用资源管理和监控设计。 可能需要注意避免直接复制引用内容,而是用自己的话总结,并正确标注来源。同时确保回答结构清晰,分点说明,每个部分有最佳实践或工具推荐,并附带示例代码或配置。</think>### Go语言工具链与最佳实践指南 #### 一、依赖管理工具 Go Modules 是官方推荐的依赖管理工具,通过$go mod init$初始化模块,自动解析依赖版本。 ```bash # 初始化模块 go mod init your_module_name # 添加依赖 go get github.com/gin-gonic/gin@v1.8.1 ``` 支持语义化版本控制和私有仓库依赖[^1]。 #### 二、测试框架 1. **原生testing包** Go内置$testing$包支持单元测试和基准测试,结合表格驱动测试提升覆盖率: ```go func TestAdd(t *testing.T) { cases := []struct{ a, b, want int }{ {2, 3, 5}, {0, 0, 0}, } for _, c := range cases { got := Add(c.a, c.b) if got != c.want { t.Errorf("Add(%d,%d) = %d, want %d", c.a, c.b, got, c.want) } } } ``` 2. **第三方库testify** 提供断言和Mock功能,简化测试代码: ```go import "github.com/stretchr/testify/assert" func TestDivide(t *testing.T) { assert.Equal(t, 2, Divide(4, 2)) } ``` 使用$go test -cover$查看覆盖率[^2]。 #### 三、部署方法 1. **容器化部署** 通过Docker打包应用,确保环境一致性: ```Dockerfile FROM golang:1.19-alpine WORKDIR /app COPY go.mod ./ RUN go mod download COPY . . RUN go build -o /go-app CMD ["/go-app"] ``` 使用Docker Compose编排多服务依赖。 2. **云原生部署** 结合Kubernetes实现自动扩缩容,利用Helm管理部署模板。 #### 四、日志处理最佳实践 1. **结构化日志** 使用$logrus$或$zap$库生成JSON格式日志,便于ELK(Elasticsearch+Logstash+Kibana)分析: ```go import log "github.com/sirupsen/logrus" func main() { log.SetFormatter(&log.JSONFormatter{}) log.WithFields(log.Fields{"event": "startup"}).Info("Service initialized") } ``` 2. **分级与控制** 动态调整日志级别(DEBUG/INFO/WARN),通过环境变量配置输出目标(文件/Syslog)。 #### 五、微服务框架 1. **GoKit** 提供服务发现、限流熔断等组件,适合构建企业级微服务: ```go import "github.com/go-kit/kit/endpoint" func makeEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(Request) result, err := svc.Process(req) return Response{Result: result}, err } } ``` 2. **GoMicro** 内置服务注册、负载均衡和RPC通信,支持插件化扩展[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值