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 是否启用LogbookFilter true
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: truesecure-filter:enabled: trueformat:style: jsonminimum-status: 400obfuscate:headers:- Authorization- X-Secretparameters:- access_token- passwordpaths:- user_idwrite:chunk-size: 1000max-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 {@Beanpublic 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 请求参数 request access_token
PathFilter 路径 request
HeaderFilter 请求头 request/response Authorization
BodyFilter Content-Type and body request/response json格式:access_token和refresh_token,form表单:client_secret和password
RequestFilter HttpRequest request 替换二进制、文件上传和流
ResponseFilter HttpResponse response 替换二进制、文件上传和流

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 {@Beanpublic QueryFilter queryFilter() {// 当前Query中的参数access_token字段被混淆成了***return QueryFilters.replaceQuery("access_token", "***");}@Beanpublic PathFilter pathFilter() {// 当前Path中的参数userId字段被混淆成了***return PathFilters.replace("userId", "***");}@Beanpublic 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"), "***");}@Beanpublic BodyFilter bodyFilter() {// 方式一BodyFilter.merge(BodyFilters.defaultValue(), new JacksonJsonFieldBodyFilter(Collections.EMPTY_SET, "***"));// 方式二return BodyFilter.merge(BodyFilters.defaultValue(),JsonBodyFilters.replaceJsonStringProperty(Collections.singleton("secret"), "***"));}@Beanpublic RequestFilter requestFilter () {return RequestFilters.replaceBody(BodyReplacers.replaceBody(Conditions.contentType("audio/*"), "<audio>"));}@Beanpublic 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/plainHello 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()
parameter body会被记录 下游代码不能使用getInputStream()
off 下游代码可以使用getParameter()或getInputStream() body不会被记录

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

Logbook HTTP日志框架相关推荐

  1. Java 日志框架适配/冲突解决方案(值得收藏)

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/6945220055399399455 前言 你是否遇到过配置了日志,但打印不出来的情况? 你是 ...

  2. 最牛逼的 Java 日志框架,性能无敌,横扫所有对手.....

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/6945753017878577165 Logback 算是JAVA 里一个老牌的日志框架,从0 ...

  3. 可能是全网最全,JAVA日志框架适配/冲突解决方案,可以早点下班了

    点击关注公众号,Java干货及时送达 你是否遇到过配置了日志,但打印不出来的情况? 你是否遇到过配置了logback,启动时却提示log4j错误的情况?像下面这样: log4j:WARN No app ...

  4. MyBatis 如何兼容所有日志框架?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来源:blog.csdn.net/zwx900102/ar ...

  5. idea log 不输出error_还在使用console.log()吗?Bunyan:一个简单易用的JS日志框架

    Bunyan是一个简单易用的JS日志框架,可以工作在多种环境下,这里以Nodejs为例说明Bunyan的基本用法.是时候替换console.log的写法了. 安装 npm install --save ...

  6. [转载]java日志框架log4j详细配置及与slf4j联合使用教程

    一.log4j基本用法 首先,配置log4j的jar,maven工程配置以下依赖,非maven工程从maven仓库下载jar添加到"build path" 1 2 3 4 5 &l ...

  7. 项目跑到到了日志警告就卡住了_java中的日志框架梳理(以故事的形式呈现)...

    日志算是java工具体系的一个知识点,但又是写代码的时候绕不过去的一个知识点,这篇文章主要是针对于小白,以故事的形式来对目前市场上常见的一些日志框架进行一个介绍和梳理. 阶段一: 从前有一个程序员,名 ...

  8. 各种Java日志框架的比较

    2019独角兽企业重金招聘Python工程师标准>>> Log4j Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台.文件.GUI组件. ...

  9. MyBatis日志到底是如何做到兼容所有常用日志框架的?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | 双子孤狼 来源 | https://blog. ...

最新文章

  1. layui 导航收缩代码_pycharm的十个小技巧,让你写代码效率翻倍
  2. Pygame实现记录事件到文本中
  3. html使用js的变量_JS变异小技巧:使用JavaScript全局变量绕过XSS过滤器
  4. STM32 定时器中断
  5. abap al设置单元格可编辑 oo_利用WPS做业务系统的超级编辑器
  6. Swagger2 使用说明
  7. mysql游标 原理解说_mysql存储过程之游标(DECLARE)原理与用法详解
  8. Java 8 新特性(二)流类库
  9. 实对称矩阵不同特征值对应的特征向量都正交
  10. linux桌面共享编程,Remmina:一个Linux下功能丰富的远程桌面共享工具
  11. Centos6 安装可视化界面
  12. 中国新型显示产业竞争格局与发展规模预测报告2022版
  13. TDSQL破圈背后:国产数据库加速出击
  14. 快速解决打印机后台程序服务没有运行的问题
  15. 任正非讲话稿400篇_伟大背后是苦难!任正非“思想之路”:400+讲话稿合集
  16. Suse12网络设置
  17. 从一元函数极值问题理解遗传算法
  18. iOS7到iOS8 一个通用的横竖屏幕切换总结
  19. git撤回上一次push
  20. android菜单键 r9,Android OPPO R9 后台 无法启动 Activity 问题

热门文章

  1. 如何给图片设置按钮的效果
  2. 2020新鲜出炉的Android大厂面试题锦集(BAT TMD JD 小米),Github标星5K 建议收藏!
  3. Java StreamTokenizer pushBack()方法与示例
  4. 玻璃纤维布规格对照表
  5. 用html编写一个红绿灯,如何用html+css+javascript写一个简易红绿灯
  6. Servlet小问题总结
  7. 路由器刷mysql_路由器刷OpenWrt打造全能服务器(七)安装mysql
  8. Snake贪吃蛇小游戏纯js代码
  9. 华为机试JAVA 免单统计
  10. Android Camera相机基础使用