Logbook HTTP日志框架

Logbook HTTP日志框架

  1. GitHub 文档及代码地址:https://github.com/zalando/logbook
  2. SpringBoot使用Logbook记录HTTP请求响应日志:https://mp.weixin.qq.com/s/9LITBfpGqTDTLbpfzp7tfA

Spring Boot的httptrace端口能够记录每次访问的请求和响应信息,但是不能记录body,这样在出问题时就不方便排查,而且httptrace不方便在原有的基础上进行扩展,所以只能寻求其他方式进行记录。

  • 允许web应用记录程序接收或发送的所有HTTP通信
  • 易于保留和进行分析

Logbook在大部分情况下是开箱即用的,即使对于一些不常用的技术或者应用,实现它们也非常简单。

特性简介

  • 日志记录:HTTP请求和响应,包含body;未授权的请求会记录部分日志(不包含body)
  • 自定义:能够自定义记录格式、记录方式以及请求记录的条件
  • 支持框架:Servlet容器、Apache’s HTTP client、Square’s OkHttp等
  • 混淆敏感数据
  • Spring Boot自动配置
  • 兼容 Scalyr
  • 合理的默认值

快速开始

Logbook为SpringBoot用户提供了很方便的自动配置功能,即我们所熟悉的starter。它使用了合理的默认值自动配置了以下功能:

  • Servlet filter
  • 适用于未授权请求的Servlet filter(如果检测到项目中使用Spring Security)
  • Header过滤器、Parameter过滤器、Body过滤器
  • HTTP格式化器、JSON格式化器
  • 日志写入方式

引入starter模块后SpringBooot会自动装配

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>2.14.0</version>
    <!--<version>${logbook.version}</version>--><!--当前的最新版是2.14.0-->
</dependency>

日志记录器必须配置为trace才能记录请求和响应。 SpringBoot可以通过将以下行添加到 application.properties 来实现

logging.level.org.zalando.logbook = trace

默认配置下,输出的日志为JSON格式:Request、Response

{
    "origin":"remote",
    "type":"request",
    "correlation":"2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
    "protocol":"HTTP/1.1",
    "sender":"127.0.0.1",
    "method":"GET",
    "path":"http://example.org/test",
    "headers":{
        "Accept":[
            "application/json"
        ],
        "Content-Type":[
            "text/plain"
        ]
    },
    "body":"Hello world!"
}
{
    "origin":"local",
    "type":"response",
    "correlation":"2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
    "duration":25,
    "protocol":"HTTP/1.1",
    "status":200,
    "headers":{
        "Content-Type":[
            "text/plain"
        ]
    },
    "body":"Hello world!"
}

配置选项

下面的展示了SpringBoot中可配置的选项:

配置项描述默认值
logbook.include仅包含某些URL(如果设置的话)[]
logbook.exclude排除某些URL(会覆盖logbook.include)设置了exclude的url是不会触发logbook[]
logbook.filter.enabled是否启用LogbookFiltertrue
logbook.filter.form-request-mode如何处理表单请求body
logbook.secure-filter.enabled是否启用SecureLogbookFilter(同时项目中使用Spring Security才会生效)true
logbook.format.style格式化样式(http,json,curl,splunk)json
logbook.strategy策略(default,status-at-least, body-only-if-status-at-least,without-body)default
logbook.minimum-status启用日志记录的最小HTTP响应状态值,当策略值为status-at-least或body-only-if-status-at-least时设置400
logbook.obfuscate.headers需要混淆的HTTP Header集合,默认脱敏符为X[Authorization]
logbook.obfuscate.paths需要混淆的path集合,默认脱敏符为X[]
logbook.obfuscate.parameters需要混淆的parameter集合,默认脱敏符为X[access_token]
logbook.write.chunk-size日志拆分块的大小,默认不拆分0 (禁用)
logbook.write.max-body-size截取Body的最大长度,后面使用...拼接
# 使用logbook需要注意的配置,设置org.zalando.logbook包的日志输出级别为trace,不然无法输出日志
logging:
  level:
    org.zalando.logbook: trace

# 如下为logbook的配置
logbook:
  include:
    - /api/**
  exclude:
    - /actuator/**
  filter:
    enabled: true
  secure-filter:
    enabled: true
  format:
    style: json
  minimum-status: 400
  obfuscate:
    headers:
      - Authorization
      - X-Secret
    parameters:
      - access_token
      - password
    paths:
      - user_id
  write:
    chunk-size: 1000
    max-body-size: 10000

详细用法

所有的功能集成都需要一个Logbook实例来完成,它保存了所有的配置并将所有需要的组件连接在一起。你可以使用所有的默认值创建一个实例:

Logbook logbook = Logbook.create();

或使用以下命令创建自定义版本:LogbookBuilder

Logbook logbook = Logbook.builder()
    .condition(new CustomCondition())
    .queryFilter(new CustomQueryFilter())
    .pathFilter(new CustomPathFilter())
    .headerFilter(new CustomHeaderFilter())
    .bodyFilter(new CustomBodyFilter())
    .requestFilter(new CustomRequestFilter())
    .responseFilter(new CustomResponseFilter())
    .sink(new DefaultSink(
            new CustomHttpLogFormatter(),
            new CustomHttpLogWriter()
    ))
    .build();

策略(Strategy)

Logbook使用一个非常硬性的策略来执行请求/响应日志记录:

  • 请求/响应分开记录
  • 请求/响应尽快记录
  • 请求/响应一起记录或不记录(即没有部分流量记录)

其中一些限制可以通过自定义HttpLogWriter实现来缓解,但都不是理想。从2.0版本开始,Logbook引入了一个新的策略模式为核心,它内置了部分策略:

  • BodyOnlyIfStatusAtLeastStrategy
  • StatusAtLeastStrategy
  • WithoutBodyStrategy

阶段(Phases)

Logbook工作在几个不同的阶段:

  1. 条件(Conditional)
  2. 过滤(Filtering)
  3. 格式化(Formatting)
  4. 记录(Writing)

每个阶段都由一个或多个可以自定义的接口完成。每个阶段都有一个合理的默认值。

条件(Conditional)

记录HTTP消息并且包含其body代价是非常大的,所以禁用某些请求的日志记录非常有意义。常见情景就是忽略一些不必要的请求,比如SpringBoot的Actuator端点。定义一个条件非常简单,只需要编写一个Predicate来决定请求是否需要记录。当然,你也可以组合预定义的 Predicate:

注意关键类:org.zalando.logbook.Conditions、org.zalando.logbook.HttpRequest

import static org.zalando.logbook.Conditions.*;

@Bean
public Logbook logbook() {
    return Logbook.builder()
        .condition(exclude(
            requestTo("/health"),
            requestTo("/admin/**"),
            contentType("application/octet-stream"),
            header("X-Secret", Stream.of("1", "X-Secret").collect(Collectors.toSet())::contains)))
        .build();
}
package com.xyz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.logbook.HttpRequest;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.zalando.logbook.Conditions.*;

@Configuration
public class LogbookConfig {
    @Bean
    public Predicate<HttpRequest> requestCondition() {
        return exclude(
            requestTo("/health"),
            requestTo("/admin/**"),
            contentType("application/octet-stream"),
            header("X-Secret", Stream.of("1", "X-Secret").collect(Collectors.toSet())::contains));
    }
}

对于路径的包含和排除也可以通过设置 logbook.include和 logbook.exclude属性实现。并且能使用通配符:例如 /admin/** 松散地遵循 Ant 的路径模式风格,而不考虑 URL 的查询字符串。

过滤(Filtering)

过滤的目的是防止记录HTTP请求和响应的某些敏感数据。通常包括Authorization请求头,但也可以用于某些明文查询或表单参数,如access_token和password

Logbook支持不同类型的过滤器:

类型作用于适用于默认值
QueryFilter请求参数requestaccess_token
PathFilter路径request
HeaderFilter请求头request/responseAuthorization
BodyFilterContent-Type and bodyrequest/responsejson格式:access_token和refresh_token,form表单:client_secret和password
RequestFilterHttpRequestrequest替换二进制、文件上传和流
ResponseFilterHttpResponseresponse替换二进制、文件上传和流

QueryFilter, PathFilter, HeaderFilter 和 BodyFilter能够满足绝大多数情况下的需求,对于更复杂的需求,可以使用 RequestFilter 和 ResponseFilter(与ForwardingHttpRequest、ForwardingHttpResponse结合使用)

import static org.zalando.logbook.HeaderFilters.authorization;
import static org.zalando.logbook.HeaderFilters.eachHeader;
import static org.zalando.logbook.QueryFilters.accessToken;
import static org.zalando.logbook.QueryFilters.replaceQuery;

Logbook logbook = Logbook.builder()
    .requestFilter(RequestFilters.replaceBody(message -> contentType("audio/*").test(message) ? "mmh mmh mmh" : null))
    .responseFilter(ResponseFilters.replaceBody(message -> contentType("*/*-stream").test(message) ? "keeps going" : null))
    .queryFilter(accessToken())
    .queryFilter(replaceQuery("password", "<secret>"))
    .headerFilter(authorization()) 
    .headerFilter(eachHeader("X-Secret"::equalsIgnoreCase, "<secret>"))
    .build();
package com.xyz.config;

import org.springframework.context.annotation.Bean;
import org.zalando.logbook.*;
import org.zalando.logbook.json.JacksonJsonFieldBodyFilter;
import org.zalando.logbook.json.JsonBodyFilters;
import java.util.Collections;

public class LogbookConfig {

    @Bean
    public QueryFilter queryFilter() {
        // 当前Query中的参数access_token字段被混淆成了***
        return QueryFilters.replaceQuery("access_token", "***");
    }

    @Bean
    public PathFilter pathFilter() {
        // 当前Path中的参数userId字段被混淆成了***
        return PathFilters.replace("userId", "***");
    }

    @Bean
    public HeaderFilter headerFilter() {
        // 替换单个字段
        HeaderFilters.replaceHeaders("Authorization", "***");
        HeaderFilter.merge(HeaderFilters.defaultValue(),HeaderFilters.replaceHeaders("filed","***"));
        // 替换多个字段,用Set集合
        HeaderFilter.merge(HeaderFilters.defaultValue(),HeaderFilters.replaceHeaders(Collections.EMPTY_SET,"***"));
        return HeaderFilters.replaceHeaders(Collections.singleton("secret"), "***");
    }

    @Bean
    public BodyFilter bodyFilter() {
        // 方式一
        BodyFilter.merge(BodyFilters.defaultValue(), new JacksonJsonFieldBodyFilter(Collections.EMPTY_SET, "***"));
        // 方式二
        return BodyFilter.merge(BodyFilters.defaultValue(),
                JsonBodyFilters.replaceJsonStringProperty(Collections.singleton("secret"), "***"));
    }

    @Bean
    public RequestFilter requestFilter () {
        return RequestFilters.replaceBody(
                BodyReplacers.replaceBody(Conditions.contentType("audio/*"), "<audio>"));
    }

    @Bean
    public ResponseFilter responseFilter () {
        return ResponseFilters.replaceBody(
                BodyReplacers.replaceBody(Conditions.contentType("*/*-stream"), "<stream>"));
    }
}

您可以根据需要配置任意数量的过滤器 - 它们将连续运行。


JsonPath body 过滤(实验),您可以将 JSONPath 过滤应用于 JSON 正文。 这里有些例子:

import static org.zalando.logbook.json.JsonPathBodyFilters.jsonPath;
import static java.util.regex.Pattern.compile;

Logbook logbook = Logbook.builder()
    .bodyFilter(jsonPath("$.password").delete())
    .bodyFilter(jsonPath("$.active").replace("unknown"))
    .bodyFilter(jsonPath("$.address").replace("X"))
    .bodyFilter(jsonPath("$.name").replace(compile("^(\\w).+"), "$1."))
    .bodyFilter(jsonPath("$.friends.*.name").replace(compile("^(\\w).+"), "$1."))
    .bodyFilter(jsonPath("$.grades.*").replace(1.0))
    .build();

在应用过滤之前和之后:

{
    "id": 1,
    "name": "Alice",
    "password": "s3cr3t",
    "active": true,
    "address": "Anhalter Straße 17 13, 67278 Bockenheim an der Weinstraße",
    "friends": [
        {
            "id": 2,
            "name": "Bob"
        },
        {
            "id": 3,
            "name": "Charlie"
        }
    ],
    "grades": {
        "Math": 1.0,
        "English": 2.2,
        "Science": 1.9,
        "PE": 4.0
    }
}
{
    "id": 1,
    "name": "Alice",
    "active": "unknown",
    "address": "XXX",
    "friends": [
        {
            "id": 2,
            "name": "B."
        },
        {
            "id": 3,
            "name": "C."
        }
    ],
    "grades": {
        "Math": 1.0,
        "English": 1.0,
        "Science": 1.0,
        "PE": 1.0
    }
}
格式化(Formatting)

格式化是把请求和响应转换为字符串。格式化不会指定请求和响应的记录位置,这是由Writer完成的。Logbook有两种默认格式化:HTTP 和 JSON

HTTP

HTTP 是默认的格式化样式,由DefaultHttpLogFormatter提供。 它主要用于本地开发和调试,而不是用于生产用途。 这是因为它不像JSON那样易于读取SpringBoot只需要配置:logbook.format.style=http,如下是 Request 和 Response

Incoming Request: 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b
GET http://example.org/test HTTP/1.1
Accept: application/json
Host: localhost
Content-Type: text/plain

Hello world!
Outgoing Response: 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b
Duration: 25 ms
HTTP/1.1 200
Content-Type: application/json

{"value":"Hello world!"}
JSON

JSON 是另一种格式样式,由 JsonHttpLogFormatter 提供。 与 HTTP 不同,它主要是为生产使用而设计的——解析器和日志消费者可以轻松地使用它。SpringBoot只需要配置:logbook.format.style=json(默认也是JSON),如下是 Request 和 Response

{
    "origin":"remote",
    "type":"request",
    "correlation":"2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
    "protocol":"HTTP/1.1",
    "sender":"127.0.0.1",
    "method":"GET",
    "path":"http://example.org/test",
    "headers":{
        "Accept":[
            "application/json"
        ],
        "Content-Type":[
            "text/plain"
        ]
    },
    "body":"Hello world!"
}
{
    "origin":"local",
    "type":"response",
    "correlation":"2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
    "duration":25,
    "protocol":"HTTP/1.1",
    "status":200,
    "headers":{
        "Content-Type":[
            "text/plain"
        ]
    },
    "body":"Hello world!"
}
Common Log Format

通用日志格式 (CLF) 是 Web 服务器在生成服务器日志文件时使用的标准化文本文件格式。 通过 CommonsLogFormatSink 支持该格式:

185.85.220.253 - - [02/Aug/2019:08:16:41 0000] "GET /search?q=zalando HTTP/1.1" 200 -
cURL

cURL 是另一种格式样式,由 CurlHttpLogFormatter 提供,它将请求呈现为可执行的 cURL 命令。 与 JSON 不同,它主要是为人类设计的。SpringBoot只需要配置:logbook.format.style=curl,如下是 Request 和 Response

curl -v -X GET 'http://localhost/test' -H 'Accept: application/json'
Outgoing Response: 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b
Duration: 25 ms
HTTP/1.1 200
Content-Type: application/json

{"value":"Hello world!"}
Splunk

Splunk 是另一种格式样式,由 SplunkHttpLogFormatter 提供,它将请求和响应呈现为键值对。SpringBoot只需要配置:logbook.format.style=splunk,如下是 Request 和 Response

origin=remote type=request correlation=2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b protocol=HTTP/1.1 sender=127.0.0.1 method=POST uri=http://example.org/test host=example.org scheme=http port=null path=/test headers={Accept=[application/json], Content-Type=[text/plain]} body=Hello world!
origin=local type=response correlation=2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b duration=25 protocol=HTTP/1.1 status=200 headers={Content-Type=[text/plain]} body=Hello world!
记录(Writer)

Writer定义了格式化后的请求和响应写入的位置。Logbook内置了三种实现:Logger、Stream、Chunking

Logger

默认情况下,使用 org.zalando.logbook.Logbook 类别和日志级别 trace 的 slf4j 记录器记录请求和响应。 也可以自定义:

默认情况下,请求和响应使用了slf4j来进行日志记录,日志的级别为 trace。也可以自定义:

Logbook logbook = Logbook.builder()
    .sink(new DefaultSink(
            new DefaultHttpLogFormatter(),
            new DefaultHttpLogWriter()
    ))
    .build();
Stream

另一种实现是将请求和响应记录到 PrintStream,例如 System.out 或 System.err。 在生产环境中这是一个糟糕的选择,但有时对短期本地开发和/或调查很有用

Logbook logbook = Logbook.builder()
    .sink(new DefaultSink(
            new DefaultHttpLogFormatter(),
            new StreamHttpLogWriter(System.err)
    ))
    .build();
Chunking

ChunkingSink 会把长的消息分割成较小的块,并且会委托给另一个sink将它们写入,只需要设置 logbook.write.chunk-size属性即可

Logbook logbook = Logbook.builder()
    .sink(new ChunkingSink(sink, 1000))
    .build();
关联ID(Correlation)

在SpringCloud应用中一般会集成Zipkin进行链路追踪,此时可以使用TraceId来关联请求和响应日志记录。

Logbook使用一个id来关联请求和响应,因为请求和响应通常位于日志文件中的不同位置(默认ID是16位随机字符组成)如默认不满足可以自定义实现:

@Bean
public org.zalando.logbook.CorrelationId correlationId () {
    return request -> UUID.randomUUID().toString();
}

Logbook logbook = Logbook.builder().correlationId(new CustomCorrelationId()).build();
Sink

HttpLogFormatter 和 HttpLogWriter 的组合能够适用于大部分场合,但也有一些局限性。 实现 Sink 接口允许更复杂的用例,例如把请求和响应持久化到数据库。你可以使用 CompositeSink 将多个Sink合并为一个。

其他框架支持

Servlet

在Servlet环境中,Logbook是通过 LogbookFilter来实现的。默认情况下,对于application/x-www-form-urlencoded请求会同等对待,即你会在日志中看到请求body。这种方法的缺点是下游代码将无法使用任何 HttpServletRequest.getParameter*(…)方法。

从Logbook 1.5.0开始,可使用 logbook.servlet.form-request系统属性(System Property)指定三种策略之一,这些策略定义Logbook如何处理这种情况(可以从源码中查看:(org.zalando.logbook.servlet.FormRequestMode)

属性值优点缺点
body(默认)body会被记录下游代码不能使用getParameter()
parameterbody会被记录下游代码不能使用getInputStream()
off下游代码可以使用getParameter()或getInputStream()body不会被记录

Logbook默认还提供了对:Servlet、HTTP Client、JAX-RS、Netty、OkHttp v2.x、OkHttp v3.x的支持,具体使用方法可以参考官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值