由于我司目前的项目都运行在 Google Cloud Platform (以下简称 GCP) 上,那么自然而然的,我们选择了使用 GCP 的 Logging 来查看日志。在使用过程中,我们发现了一个问题,那就是我们无法直观的看到日志是从什么地方打印出来的,经常需要通过日志内容,在代码里面通过全文搜索来定位。这样就产生了一个需求:可不可以把这条日志所在的类、方法,和行数一起打印在日志中?
日志是怎么生成的 既然要实现自动化地修改日志的内容,那么我们首先得要知道,日志的内容是怎么产生的。根据我们的 logback-spring.xml
中的配置,我们可以定位到,日志是从 StackdriverJsonLayout
这个类中生成出来的。
打开这个文件,阅读源码,发现有一个名为 toJsonMap
的方法很像我们这次的目标,为了大家省事,我从 GitHub 上把这段源码拿到了这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Override protected Map<String, Object> toJsonMap (ILoggingEvent event) { Map<String, Object> map = new LinkedHashMap <>(); if (this .includeMDC) { event.getMDCPropertyMap().forEach((key, value) -> { if (!FILTERED_MDC_FIELDS.contains(key)) { map.put(key, value); } }); } if (this .includeTimestamp) { map.put(StackdriverTraceConstants.TIMESTAMP_SECONDS_ATTRIBUTE, TimeUnit.MILLISECONDS.toSeconds(event.getTimeStamp())); map.put(StackdriverTraceConstants.TIMESTAMP_NANOS_ATTRIBUTE, TimeUnit.MILLISECONDS.toNanos(event.getTimeStamp() % 1_000 )); } add(StackdriverTraceConstants.SEVERITY_ATTRIBUTE, this .includeLevel, String.valueOf(event.getLevel()), map); add(JsonLayout.THREAD_ATTR_NAME, this .includeThreadName, event.getThreadName(), map); add(JsonLayout.LOGGER_ATTR_NAME, this .includeLoggerName, event.getLoggerName(), map); if (this .includeFormattedMessage) { String message = event.getFormattedMessage(); if (this .includeExceptionInMessage) { IThrowableProxy throwableProxy = event.getThrowableProxy(); if (throwableProxy != null ) { String stackTrace = getThrowableProxyConverter().convert(event); if (stackTrace != null && !stackTrace.equals("" )) { message += "\n" + stackTrace; } } } map.put(JsonLayout.FORMATTED_MESSAGE_ATTR_NAME, message); } add(JsonLayout.MESSAGE_ATTR_NAME, this .includeMessage, event.getMessage(), map); add(JsonLayout.CONTEXT_ATTR_NAME, this .includeContextName, event.getLoggerContextVO().getName(), map); addThrowableInfo(JsonLayout.EXCEPTION_ATTR_NAME, this .includeException, event, map); addTraceId(event, map); add(StackdriverTraceConstants.SPAN_ID_ATTRIBUTE, this .includeSpanId, event.getMDCPropertyMap().get(StackdriverTraceConstants.MDC_FIELD_SPAN_ID), map); if (this .serviceContext != null ) { map.put(StackdriverTraceConstants.SERVICE_CONTEXT_ATTRIBUTE, this .serviceContext); } if (this .customJson != null && !this .customJson.isEmpty()) { for (Map.Entry<String, Object> entry : this .customJson.entrySet()) { map.putIfAbsent(entry.getKey(), entry.getValue()); } } addCustomDataToJsonMap(map, event); return map; }
看起来篇幅好像很大,但是仔细一看,不难明白,它就是生成了一个 Map
,而这个 Map
里面放的,就是最后我们在 GCP Logging 里面将要看到的日志。好了,目标找到了,那就准备动手吧。
修改日志内容 要修改的方法找到了,但是我们仍不知道应该怎么取得类名、方法和行号。经过又一顿网上冲浪,加上在源码里面一刨一刨的翻,和一点小小的灵感,最后得知这些信息全部可以通过 ILoggingEvent#getCallerData()
方法取得。请原谅因为时间过去的有点久,我已经找不到当时给我指路的文章,所以也就不能放在下面的参考中了。
那么我们就开始着手实现这个需求吧。首先在项目里面新建一个类,因为我们的目的是重写 toJsonMap
这个方法,所以要让它继承 StackdriverJsonLayout
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class GcpLoggingLayout extends StackdriverJsonLayout { @Override public Map<String, Object> toJsonMap (ILoggingEvent event) { Map<String, Object> map = super .toJsonMap(event); StackTraceElement[] callerData = event.getCallerData(); if (callerData == null || callerData.length == 0 ) { return map; } StackTraceElement stackTraceElement = callerData[0 ]; String[] classNameParts = stackTraceElement.getClassName().split("\\." ); String callerClass = classNameParts[classNameParts.size - 1 ]; String methodName = stackTraceElement.getMethodName(); String lineNumber = stackTraceElement.getLineNumber().toString(); String originalMessage = map.get(JsonLayout.FORMATTED_MESSAGE_ATTR_NAME); String enhancedMessage = "class=" + callerClass + " " + "method=" + methodName + " " + "line=" + lineNumber + " " + originalMessage; map.put(JsonLayout.FORMATTED_MESSAGE_ATTR_NAME, enhancedMessage); return map; } }
这样我们的日志里就带上了类名、方法名,和行号了。但是到了这里并没有结束,因为上面的代码有一个问题:框架打出来的日志也带上了这些信息。虽然不影响使用,但是它不好看,我不想要。所以我决定继续优化一下。
继续优化 稍加思索,我提出了两个新的需求:
要根据包名过滤,只给指定的包里面的类加上这些信息。这样,我就可以排除掉所有无关的包。
还要根据类名过滤,遇到被指定的类,我就不加上这些信息。因为对于启动类 (就是放 main
方法那个),和一些用于健康检查的类,它们打出的日志也没必要加上这些信息。
但是很明显,原厂的功能并不足以实现这两个需求。我为了这两个需求,新增了两个参数:appPackage
用来指定应用的包名,excludedClassNames
用来指定要被排除的类。
增加了参数,那必然也要修改代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public class GcpLoggingLayout extends StackdriverJsonLayout { private String appPackage; private List<String> excludedClassNames; public void setAppPackage (String appPackage) { this .appPackage = appPackage; } public String getAppPackage () { return this .appPackage; } public void setExcludedClassNames (String excludedClassNames) { this .excludedClassNames = Arrays.stream(excludedClassNames.split("," )) .map(String::trim) .collect(Collectors.toList()); } public List<String> getExcludedClassNames () { return this .excludedClassNames; } @Override public Map<String, Object> toJsonMap (ILoggingEvent event) { Map<String, Object> map = super 。toJsonMap(event); StackTraceElement[] callerData = event.getCallerData(); if (callerData == null || callerData.length == 0 ) { return map; } StackTraceElement stackTraceElement = callerData[0 ]; String callerClassFullName = stackTraceElement.getClassName(); if (!callerClassFullName.contains(appPackage)) { return map; } String[] classNameParts = stackTraceElement.getClassName().split("\\." ); String callerClass = classNameParts[classNameParts.size - 1 ]; if (excludedClassNames.contains(callerClass)) { return map; } String methodName = stackTraceElement.getMethodName(); String lineNumber = String.valueOf(stackTraceElement.getLineNumber()); String originalMessage = map.get(JsonLayout.FORMATTED_MESSAGE_ATTR_NAME).toString(); String enhancedMessage = "class=" + callerClass + " " + "method=" + methodName + " " + "line=" + lineNumber + " " + originalMessage; map.put(JsonLayout.FORMATTED_MESSAGE_ATTR_NAME, enhancedMessage); return map; } }
除了修改代码,日志配置文件 logback-spring.xml
也要针对我们新增的两个属性作出修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <include resource ="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" /> <include resource ="org/springframework/cloud/gcp/logging/logback-json-appender.xml" /> <include resource ="org/springframework/boot/logging/logback/defaults.xml" /> <appender name ="CONSOLE_JSON" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.core.encoder.LayoutWrappingEncoder" > <layout class ="com.boris1993.myapplication.config.GcpLoggingLayout" > <appPackage > com.boris1993.myapplication</appPackage > <excludedClassNames > APP, HealthCheck</excludedClassNames > </layout > </encoder > </appender > </configuration >
然后就可以部署到 GCP 上面看效果了,或者在本机使用对应的 profile 启动,然后检查控制台里面打印出来的 JSON。
[^1]: Stackdriver Logging [^2]: StackdriverJsonLayout.java - GitHub