Java的话本地打断点可以调试获取rest入参(http header),但是在生产环境可能我们获取入参(Http header/parameter)可能就没有那么的轻松了。我们可能在header中放置了很多自定的参数用来鉴权或者其他用途。如果排查问题的时候需要这些参数,我们有很多种选择去获取这些参数。
- 输出到应用日志中,比如使用logback,log.error(xxx)
- 借助nginx 输出到access.log日志中
- 借助Skywalking/zipkin等中间件输出到链路中
- 网关日志中输出
1. 输出到应用日志中
我们可以借助Springboot的拦截器在进入rest controller 之前将request header / param 输出出来,在rest controller调用结束之后将response header / param输出。
LogInterceptor
拦截器,注意拦截器和过滤器的区别,过滤器属于Tomcat/Jetty/… Servlet 容器的生命周期维护的,要早于拦截器。过滤器是Springboot维护的拦截,在handler mapping 映射之后先去调用拦截器之后在调用controller。
拦截器拦截的就是上图4的这部分。
@Component
@Slf4j
public class LogInterceptor extends HandlerInterceptorAdapter {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
threadLocal.set(System.currentTimeMillis());
log.info("Request uri = [{}], method is: [{}]", request.getRequestURI(), request.getMethod());
log.info("Request header is : [{}]", parseRequestHeaders(request));
log.info("Request param is : [{}]", parseParams(request));
if (request instanceof RequestCustomWrapper) {
RequestCustomWrapper requestCustomWrapper = (RequestCustomWrapper) request;
byte[] body = requestCustomWrapper.getBody();
log.info("Request body is : [{}]", new String(body));
}
return super.preHandle(request, response, handler);
}
public static String parseParams (HttpServletRequest request) {
StringBuilder stringBuilder = new StringBuilder();
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
request.getParameter(name);
stringBuilder.append(name).append("=").append(";");
}
return stringBuilder.toString();
}
public static String parseRequestHeaders (HttpServletRequest request) {
StringBuilder stringBuilder = new StringBuilder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
stringBuilder.append(name).append("=").append(value).append(";");
}
return stringBuilder.toString();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
}
}
RequestCustomWrapper
因为Springboot框架规定,Request getInputStream只能获取一次,获取第二次的时候就会报错。所以这个时候需要实现RequestWrapper去包裹Request重写getInputStream实现可重复获取Request Stream。
@Slf4j
public class RequestCustomWrapper extends HttpServletRequestWrapper {
private byte[] body;
public byte[] getBody() {
return body;
}
public RequestCustomWrapper(HttpServletRequest request) {
super(request);
try {
body = readBytes(request.getReader());
} catch (IOException e) {
log.error("读取request input stream失败..");
}
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
try (final ByteArrayInputStream bais = new ByteArrayInputStream(body)) {
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
public byte[] readBytes (BufferedReader br) throws IOException {
byte[] emptyBytes = new byte[0];
String str;
StringBuilder sb = new StringBuilder();
while ((str = br.readLine()) != null) {
sb.append(str);
}
if (StringUtils.isNotBlank(sb.toString())) {
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
return emptyBytes;
}
}
RequestCustomFilter
全局过滤器到,在执行到RequestCustomFilter
这一层的时候,将ServletRequest包裹替换成自己的Request,实现可重复获取Request Stream.
public class RequestCustomFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
ServletRequest requestWrapper = new RequestCustomWrapper((HttpServletRequest) servletRequest);
filterChain.doFilter(requestWrapper, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
WebMvcConfig
注册过滤器和拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/**");
}
@Bean
public FilterRegistrationBean<RequestCustomFilter> requestCustomFilter () {
FilterRegistrationBean<RequestCustomFilter> registrationBean = new FilterRegistrationBean<>();
RequestCustomFilter requestCustomFilter = new RequestCustomFilter();
registrationBean.setFilter(requestCustomFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
return registrationBean;
}
}
2.Nginx 配置输出Log
配置方式:
在nginx的配置文件中有个变量:$http_cookie来获取cookie的信息。配置方式很简单,只需要在nginx配置文件的http段,新添加一个log_format就可以了:nginx.conf
log_format sendfile '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$http_cookie"';
在server.conf中加入
access_log /var/log/php/access.log sendfile;
配置层级结构sendfile
就是上面定义的log_format 的名字,只要acces_log
后面带这个名字的日志,就会按照定义的格式输出日志。
reload一下nginx就可以在日志里面看到cookie
信息
nginx -s reload
Nginx 变量参考
-
$remote_addr
#存放了客户端的地址,注意是客户端的公⽹IP -
$args
#变量中存放了URL中的指令http://www.magedu.net/main/index.do?id=090&partner=search以上:id=090&partner=search 即为 $args -
$document_root
#保存了针对当前资源的请求的系统根⽬录,如/apps/nginx/html -
$cookie_name
#表⽰key为 name 的cookie值 -
$document_uri
#保存了当前请求中不包含指令的URI,注意是不包含请求的指令,如http://www.magedu.net/main/index.do?id=090&partner=search
会被定义为/main/index.do
-
$host;
#存放了请求的host名称 -
$http_user_agent
#客户端浏览器的详细信息 -
$http_cookie
#客户端的cookie信息 -
$limit_rate
#如果nginx服务器使⽤limit_rate配置了显⽰⽹络速率,则会显⽰,如果没有设置, 则显⽰0 -
$remote_port
#客户端请求Nginx服务器时客户端随机打开的端⼝ -
$remote_user
#已经经过Auth Basic Module验证的⽤户名 -
$request_body_file
#做反向代理时发给后端服务器的本地资源的名称 -
$request_method
#请求资源的⽅式,GET/PUT/DELETE等 -
$request_filename
#当前请求的资源⽂件的路径名称,由root或alias指令与URI请求⽣成的⽂件绝对路径,
3. 借助Skywalking/zipkin等中间件输出到链路中
请关注我,后续会单独出一期Skywalking的教程以及Skywalking原理讲解。文章来源:https://www.toymoban.com/news/detail-422813.html
4. 网关日志中输出
这里只简单贴一下代码介绍网管如何打印Http信息,后续会单独出一期介绍网关的文章。请关注我!!!文章来源地址https://www.toymoban.com/news/detail-422813.html
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {
private static final String START_TIME = "START_TIME";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String info = String.format("Method: {%s} Host: {%s} Path: {%s} Query: {%s}",
request.getMethod().name(),
request.getURI().getHost(),
request.getURI().getPath(),
request.getQueryParams());
log.info(info);
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long start = exchange.getAttribute(START_TIME);
if (start != null) {
long executeTime = System.currentTimeMillis() - start;
log.info(exchange.getRequest().getURI().getRawPath() + ":" + executeTime + "ms");
}
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
到了这里,关于Springboot 获取http数据、打印HTTP参数的4种方式 (便于生产排查问题)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!