引言

在系统构建完成之后,我们通常会使用REST API对外提供服务,在REST API的处理过程中经常会出现一些异想不到的问题(用户权限不足、参数不全、数据库访问异常等),导致请求失败,很多时候用户并不能理解这些失败是如何造成的,他们更多的是直接找到相应的开发者询问:“我的这个接口失败了,没有拿到数据,帮忙看一下吧”,更为复杂的是当我们询问其他用户的时候,他们却说:“你这个接口是正常的啊”,开发者这时就很郁闷:“我又没对你做特殊处理,怎么别人是好的,偏偏就你的失败”(本人在工作初期,经常遇到此类场景,表示很无奈)。

问题还是要解决,怎么办?找日志呗!打开终端、拿起微盾、输入密码、输入IP,等等!!!我们的REST API是部署在多台服务器上,前面有一台Nginx,“我去,谁知道是哪台服务器处理你的请求的,哦,我们在日志目录下挂载了NFS,每个REST API的日志信息会写入到一个固定的文件夹中”,“你什么时候请求这个接口的”,然后估计一个大概的时间,找到对应的日志文件(日志输出使用Log4j,并按照天进行滚动),“我X,这个接口访问如此频繁,日志全部都混在了一起,怎么找”,“对了,我们有RequestID,每个请求都是唯一的,快找”,“我X,RequestID并不是每行日志都带有的,完了”,日志无法定位,无颜面对用户呀,只好羞愧地对用户说:“我们有测试环境,我给你一个IP,你再访问一次,我看看日志就知道啥原因了”。

多么高大上的回答呀,还是掩饰不住尴尬的内心,唉...

问题终归是要解决的,把程序员(尤其是我)搞的不耐烦了,就要有创新了(自夸一下),另外得知我们的小伙伴们提供ELK服务,REST API日志可视化的想法油然而生。

注:本文仅关注在REST环境下如果自定义日志输出,不涉及ELK部分(其他小伙伴支持,团队的力量)

关键问题

REST API中使用log4j进行日志输出,如何在不影响现有代码的基础上(无须在业务代码中添加任何代码),收集一次请求的日志信息,透明的将日志输出至ELK

解决思路

(1)使用ServletRequestListener对请求过程进行监听,请求过程包含两部分:requestInitialized、requestDestroyed;
(2)每一次请求的初始化、处理、销毁是由一个独立的线程负责完成的(熟悉Java的同学可能立马就会想到ThreadLocal);
(3)现有业务代码中已经使用log4j作日志输出,为了保证不影响现有代码及以后的开发,唯一的方式,自定义Appender;

通过上述三步,我们可以大致得出这样一个流程:

(1)在ServletRequestListener requestInitialized初始化当前线程的日志对象;
(2)业务代码执行过程中,log4j输出日志时通过我们自定义的Appender,将日志信息保存至当前线程的日志对象中;
(3)在ServletRequestListener requestDestroyed中将当前线程的日志对象中的日志信息输出至目的地(这里是ELK,也可以是其它),然后清空线程对象。

注意:以上全部操作均依赖于一个请求过程的处理全部处于一个线程环境中。

解决方案

(1)定义日志对象

  1. public class DipLog {
  2.  
  3. public static class DipLogMessage {
  4.  
  5. private String time;
  6.  
  7. private String level;
  8.  
  9. private String filename;
  10.  
  11. private String className;
  12.  
  13. private String methodName;
  14.  
  15. private String lineNumber;
  16.  
  17. private String message;
  18.  
  19. public DipLogMessage(String time, String level, String filename,
  20. String className, String methodName, String lineNumber,
  21. String message) {
  22. this.time = time;
  23.  
  24. this.level = level;
  25.  
  26. this.filename = filename;
  27.  
  28. this.className = className;
  29.  
  30. this.methodName = methodName;
  31.  
  32. this.lineNumber = lineNumber;
  33.  
  34. this.message = message;
  35. }
  36.  
  37. public String getTime() {
  38. return time;
  39. }
  40.  
  41. public String getLevel() {
  42. return level;
  43. }
  44.  
  45. public String getFilename() {
  46. return filename;
  47. }
  48.  
  49. public String getClassName() {
  50. return className;
  51. }
  52.  
  53. public String getMethodName() {
  54. return methodName;
  55. }
  56.  
  57. public String getLineNumber() {
  58. return lineNumber;
  59. }
  60.  
  61. public String getMessage() {
  62. return message;
  63. }
  64.  
  65. @Override
  66. public String toString() {
  67. return String.format("%s\t%s\t%s\t%s\t%s\t%s\t%s", time, level,
  68. filename, className, methodName, lineNumber, message);
  69. }
  70.  
  71. }
  72.  
  73. private Map<String, String> properties = new HashMap<String, String>();
  74.  
  75. private List<DipLogMessage> messages = Collections
  76. .synchronizedList(new ArrayList<DipLog.DipLogMessage>());
  77.  
  78. public void addProperty(String key, String value) {
  79. properties.put(key, value);
  80. }
  81.  
  82. public void addMessage(DipLogMessage message) {
  83. messages.add(message);
  84. }
  85.  
  86. public Map<String, String> getProperties() {
  87. return properties;
  88. }
  89.  
  90. public List<DipLogMessage> getMessages() {
  91. return messages;
  92. }
  93.  
  94. }

properties中保存一些自定义属性值(log4j本身不支持的),messages中保存通过log4j debug、info、warn、error输出的日志消息(DipLogMessage )。

(2)通过ThreadLocal保存一个请求处理过程中的日志对象

  1. public class DipLogThreadLocal {
  2.  
  3. private static final ThreadLocal<DipLog> DIP_LOG_THREAD_LOCAL = new ThreadLocal<DipLog>();
  4.  
  5. public static DipLog get() {
  6. return DIP_LOG_THREAD_LOCAL.get();
  7. }
  8.  
  9. public static void set(DipLog dipLog) {
  10. DIP_LOG_THREAD_LOCAL.set(dipLog);
  11. }
  12.  
  13. public static void clear() {
  14. DIP_LOG_THREAD_LOCAL.set(null);
  15. }
  16.  
  17. }

(3)扩展Log4j,自定义Appender,将请求处理过程中的日志消息保存至当前线程关联的日志对象中

  1. public class DipLogAppender extends WriterAppender {
  2.  
  3. private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat(
  4. "yyyy-MM-dd HH:mm:ss,SSS");
  5.  
  6. @Override
  7. public void append(LoggingEvent event) {
  8. String time = DATETIME_FORMAT.format(new Date(event.getTimeStamp()));
  9.  
  10. String level = event.getLevel().toString();
  11.  
  12. String filename = event.getLocationInformation().getFileName();
  13.  
  14. String className = event.getLocationInformation().getClassName();
  15.  
  16. String methodName = event.getLocationInformation().getMethodName();
  17.  
  18. String lineNumber = event.getLocationInformation().getLineNumber();
  19.  
  20. String message = event.getRenderedMessage();
  21.  
  22. DipLogMessage dipLogMessage = new DipLogMessage(time, level, filename,
  23. className, methodName, lineNumber, message);
  24.  
  25. DipLog dipLog = DipLogThreadLocal.get();
  26.  
  27. dipLog.addMessage(dipLogMessage);
  28. }
  29.  
  30. }

(4)创建ServletRequestListener

  1. public class DipLogRequestListener implements ServletRequestListener {
  2.  
  3. @Override
  4. public void requestInitialized(ServletRequestEvent event) {
  5. DipLog dipLog = new DipLog();
  6.  
  7. dipLog.addProperty("requestId",
  8. String.valueOf(System.currentTimeMillis()));
  9. dipLog.addProperty("url", ((HttpServletRequest) event
  10. .getServletRequest()).getRequestURI());
  11.  
  12. DipLogThreadLocal.set(dipLog);
  13. }
  14.  
  15. @Override
  16. public void requestDestroyed(ServletRequestEvent event) {
  17. DipLog dipLog = DipLogThreadLocal.get();
  18.  
  19. Map<String, String> properties = dipLog.getProperties();
  20. for (Entry<String, String> entry : properties.entrySet()) {
  21. System.out.println(entry.getKey() + "\t" + entry.getValue());
  22. }
  23. List<DipLogMessage> messages = dipLog.getMessages();
  24. for (DipLogMessage dipLogMessage : messages) {
  25. System.out.println(dipLogMessage);
  26. }
  27.  
  28. DipLogThreadLocal.clear();
  29. }
  30.  
  31. }

从黑体部分代码可以看出,我们在日志对象中保存着当前请求的RequestID及URL(可以添加到最后的日志消息输出),通过requestDestroyed完成请求日志消息的具体输出(这里仅仅模拟,直接输出至控制台)。

这里仅仅介绍核心实现,可以在此基础之上根据业务需求扩展出更为复杂的功能。

REST服务中的日志可视化(关键技术实现)的更多相关文章

  1. iOS中 加强日志输出 开发技术总结

    对于那些做后端开发的工程师来说,看LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是对一些不太容易重现的B ...

  2. MySQL系列详解三:MySQL中各类日志详解-技术流ken

    前言 日志文件记录了MySQL数据库的各种类型的活动,MySQL数据库中常见的日志文件有 查询日志,慢查询日志,错误日志,二进制日志,中继日志 .下面分别对他们进行介绍. 查询日志 1.查看查询日志变 ...

  3. 使用日志服务进行Kubernetes日志采集

    阿里云容器服务Kubernetes集群集成了日志服务(SLS),您可在创建集群时启用日志服务,快速采集Kubernetes 集群的容器日志,包括容器的标准输出以及容器内的文本文件. 新建 Kubern ...

  4. 在线公开课 | 5G时代的视频云服务关键技术与实践

    5G时代来临在即,视频技术将会如何发展呢?基于视频云服务的技术开发又有怎样的变化呢?为此,京东云视频云产品研发部高级总监魏伟为大家做出了详细解读. 魏伟拥有10多年视频行业研发经验,先后从事于AVS标 ...

  5. Java中,多态的实现有哪些要求?实现多态的关键技术?

     多态指的是允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用).实现多态的方法是动态绑定( Dynamic Binding),动态绑定 ...

  6. sqlserver服务启动后停止,传递给数据库 'master' 中的日志扫描操作的日志扫描号无效

    电脑异常重启,导致SqlServer服务启动后,自动停止,在[计算机管理]-[事件查看器]-[windows日志]中进行查看系统错误日志,在[应用程序]下发现可能的错误信息有以下两条: 1.错误:传递 ...

  7. 互联网DSP广告系统架构及关键技术解析

    互联网DSP广告系统架构及关键技术解析 宿逆 关注 1.9 2017.10.09 17:05* 字数 8206 阅读 10271评论 2喜欢 60 广告和网络游戏是互联网企业主要的盈利模式 广告是广告 ...

  8. DSP广告系统架构及关键技术解析(转)

    广告和网络游戏是互联网企业主要的盈利模式 广告是广告主通过媒体以尽可能低成本的方式与用户达成接触的商业行为.也就是说按照某种市场意图接触相应人群,影响其中潜在用户,使其选择广告主产品的几率增加,或对广 ...

  9. Java Hotspot G1 GC的一些关键技术

    G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推出,相 ...

随机推荐

  1. [Angualr 2] Watch for changes

    You can watch for form / control changes by using .valueChanges.observe({...}): this.sku.valueChange ...

  2. Lucene中string docvalues使用utf-16的优化

    原来的string docvalues使用utf-8编码,载入时转码花费大量时间,我们把转码实现从new String(bytes, "UTF-8")改用lucene的bytesR ...

  3. android之interpolator的用法详解

    Android:interpolator Interpolator 被用来修饰动画效果,定义动画的变化率,可以使存在的动画效果accelerated(加速),decelerated(减速),repea ...

  4. PHP安全编程:HTTP请求欺骗(转)

    一个比欺骗表单更高级和复杂的攻击方式是HTTP请求欺骗.这给了攻击者完全的控制权与灵活性,它进一步证明了不能盲目信任用户提交的任何数据. 为了演示这是如何进行的,请看下面位于http://exampl ...

  5. 在VisualStudio 2012中通过SmallSharp压缩js及修改web.config

    在项目中加入一个targets文件,取名my.build.targets 在targets文件中加入内容: <?xml version="1.0" encoding=&quo ...

  6. AutoBackupForApps

    This sample demonstrates how to selectively disable Automatic Backups in Android M, either by adjust ...

  7. 详解Android Handler的使用-别说你不懂handler(转)

    我们进行Android开发时,Handler可以说是使用非常频繁的一个概念,它的用处不言而喻.本文就详细介绍Handler的基本概念和用法. Handler的基本概念         Handler主 ...

  8. Oracle 存储过程(2)

    http://www.cnblogs.com/chinafine/archive/2010/07/12/1776102.html http://blog.itpub.net/29485627/view ...

  9. codevs1404字符串匹配

    /* 无奈我改了那么久还是看的题解 首先跑一边kmp 几下ans[p]表示总共匹配到长度p的次数 这些不一定都是恰好到p 所以在处理一下 ans[p]通过处理变成 所有的匹配到长度p的次数 最后答案就 ...

  10. 本文实例汇总了C#中@的用法,对C#程序设计来说有不错的借鉴价值。

    具体如下: 一 字符串中的用法 1.学过C#的人都知道C# 中字符串常量可以以@ 开头声名,这样的优点是转义序列“不”被处理,按“原样”输出,即我们不需要对转义字符加上 \ (反斜扛),就可以轻松co ...