REST服务中的⽇志可视化(关键技术实现)
引⾔
在系统构建完成之后,我们通常会使⽤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)定义⽇志对象
public class DipLog {
public static class DipLogMessage {
private String time;
private String level;
private String filename;
private String className;
private String methodName;
private String lineNumber;
private String message;
public DipLogMessage(String time, String level, String filename,
String className, String methodName, String lineNumber,
String message) {
this.time = time;
this.level = level;
this.filename = filename;
this.className = className;
this.lineNumber = lineNumber;
}
public String getTime() {
return time;
}
public String getLevel() {
return level;
}
public String getFilename() {
return filename;
}
public String getClassName() {
return className;
}
public String getMethodName() {
return methodName;
}
public String getLineNumber() {
return lineNumber;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return String.format("%s\t%s\t%s\t%s\t%s\t%s\t%s", time, level,
filename, className, methodName, lineNumber, message);
}
}
private Map<String, String> properties = new HashMap<String, String>();
private List<DipLogMessage> messages = Collections
.synchronizedList(new ArrayList<DipLog.DipLogMessage>());
public void addProperty(String key, String value) {
properties.put(key, value);
}
public void addMessage(DipLogMessage message) {
messages.add(message);
}
public Map<String, String> getProperties() {
return properties;
}
public List<DipLogMessage> getMessages() {
return messages;
}
}
properties中保存⼀些⾃定义属性值(log4j本⾝不⽀持的),messages中保存通过log4j debug、info、warn、error输出的⽇志消息(DipLogMessage )。
(2)通过ThreadLocal保存⼀个请求处理过程中的⽇志对象
public class DipLogThreadLocal {
private static final ThreadLocal<DipLog> DIP_LOG_THREAD_LOCAL = new ThreadLocal<DipLog>();
public static DipLog get() {
return DIP_LOG_();
}
public static void set(DipLog dipLog) {
DIP_LOG_THREAD_LOCAL.set(dipLog);
}log4j2 appender
public static void clear() {
DIP_LOG_THREAD_LOCAL.set(null);
}
}
(3)扩展Log4j,⾃定义Appender,将请求处理过程中的⽇志消息保存⾄当前线程关联的⽇志对象中public class DipLogAppender extends WriterAppender {
private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss,SSS");
@Override
public void append(LoggingEvent event) {
String time = DATETIME_FORMAT.format(new TimeStamp()));
String level = Level().toString();
String filename = LocationInformation().getFileName();
String className = LocationInformation().getClassName();
String methodName = LocationInformation().getMethodName();
String lineNumber = LocationInformation().getLineNumber();
String message = RenderedMessage();
DipLogMessage dipLogMessage = new DipLogMessage(time, level, filename,
className, methodName, lineNumber, message);
DipLog dipLog = ();
dipLog.addMessage(dipLogMessage);
}
}
(4)创建ServletRequestListener
public class DipLogRequestListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent event) {
DipLog dipLog = new DipLog();
dipLog.addProperty("requestId",
String.valueOf(System.currentTimeMillis()));
dipLog.addProperty("url", ((HttpServletRequest) event
.getServletRequest()).getRequestURI());
DipLogThreadLocal.set(dipLog);
}
@Override
public void requestDestroyed(ServletRequestEvent event) {
DipLog dipLog = ();
Map<String, String> properties = Properties();
for (Entry<String, String> entry : Set()) {
System.out.Key() + "\t" + Value());
}
List<DipLogMessage> messages = Messages();
for (DipLogMessage dipLogMessage : messages) {
System.out.println(dipLogMessage);
}
DipLogThreadLocal.clear();
}
}
从⿊体部分代码可以看出,我们在⽇志对象中保存着当前请求的RequestID及URL(可以添加到最后的⽇志消息输出),通过requestDestroyed完成请求⽇志消息的具体输出(这⾥仅仅模拟,直接输出⾄控制台)。
这⾥仅仅介绍核⼼实现,可以在此基础之上根据业务需求扩展出更为复杂的功能。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论