前言
ELK是目前主流的日志收集、存储和可视化的工具。
ElasticSearch:用于存储日志信息;
Logstash:用于收集、处理和转发日志信息;
Kibana:提供可视化的Web界面.
这里主要实现的是通过logbook和logstash完成日志的收集功能。
1、添加依赖包
<!--json转换--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <!--调用远程接口--> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> <version>11.9.1</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-gson</artifactId> <version>11.9.1</version> </dependency> <!--集成logstash--> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.2</version> </dependency>
2、创建logback-spring.xml文件
在main\resources路径下创建logback-spring.xml文件,用于配置logback。
因为我的logstash需要Authorization验证,所以就自己实现了httpClient,小伙伴们需要将 Logstash的<appender>和<httpClient>中的class指向自己的项目路径。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> <springProperty scope="context" name="projectName" source="tec.project-name" defaultValue="tec"/> <springProperty scope="context" name="stage" source="stage" defaultValue="local"/> <appender name="LOCAL-CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} %5level %logger{0} [%t] [%mdc] - %msg%n</pattern> </encoder> </appender> <!--指向自己的项目文件--> <appender name="LOGSTASH" class="net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender"> <ringBufferSize>8192</ringBufferSize> <appender class="com.unicorn.tec.logstash.HttpAppender"> <!--指向自己的项目文件--> <httpClient class="com.unicorn.tec.logstash.HttpClient"> <!--logstash地址和端口号--> <destination>http://47.**.**.**:****</destination> <!--Authorization的Username和Password--> <username>username</username> <password>password</password> <topic>my-topic</topic> <app>my-app</app> <!--日志收集级别--> <level>ERROR</level> </httpClient> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <fieldNames> <timestamp>timestamp</timestamp> </fieldNames> </encoder> </appender> </appender> <root level="ERROR"> <appender-ref ref="LOGSTASH"/> <appender-ref ref="LOCAL-CONSOLE"/> </root> </configuration>
3、实现HttpAppender和HttpClient
在实现HttpAppender和HttpClient之前,我们需要先声明logstash的远程调用接口,即创建ILogstashService文件
package com.unicorn.tec.logstash; import feign.Headers; import feign.RequestLine; import feign.Response; import org.springframework.web.bind.annotation.RequestBody; public interface ILogstashService { @Headers("Content-Type: application/json") @RequestLine("PUT /") Response addLog(@RequestBody String body); }
HttpClient代码:
package com.unicorn.tec.logstash; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import ch.qos.logback.core.status.Status; import com.alibaba.fastjson.JSON; import feign.Feign; import feign.Logger; import feign.Response; import feign.auth.BasicAuthRequestInterceptor; import lombok.var; import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.net.HttpURLConnection; import java.util.*; public class HttpClient extends ContextAwareBase implements LifeCycle { private String destination; private String username; private String password; private String topic; private String app; private String level; @Value("${spring.profiles.active}") private String env; private ILogstashService logstash; private volatile boolean isStarted = false; @Override public synchronized void start(){ if(isStarted){ return; } if (destination == null || destination.isEmpty()) { addError("No encoder was configured. Use <destination> URL"); } if (hasAnyErrors()) { return; } logstash = Feign.builder() .decoder((response, type) -> response) .logLevel(Logger.Level.NONE) .requestInterceptor(new BasicAuthRequestInterceptor(username, password)) .target(ILogstashService.class, destination); isStarted = true; addInfo("HttpClient started: '" + destination + "'"); } public void addLog(ILoggingEvent event) { Level minLevel = Level.toLevel(level); if(!event.getLevel().isGreaterOrEqual(minLevel)){ return; } //自定义自己想要的内容 Map<String,Object> request = new HashMap<>(); request.put("Topic", topic); request.put("Message",event.getMessage()); request.put("Level",toLevelString(event.getLevel())); request.put("Logger",event.getLoggerName()); request.put("Time",event.getTimeStamp()); request.put("App", app); request.put("Env","dev"); request.put("Source",""); request.put("Scope",""); var dic = event.getMDCPropertyMap(); if(dic.containsKey("url")){ request.put("Url",dic.get("url")); } if(dic.containsKey("parameters")){ request.put("Parameters",dic.get("parameters")); } if(dic.containsKey("cookie")){ request.put("Session",dic.get("cookie")); } var throwable = event.getThrowableProxy(); if(throwable != null){ request.put("Exception", throwable.getMessage()); StringBuffer stackTraceString = new StringBuffer(); stackTraceString.append(throwable.getClassName()) .append(": ") .append(throwable.getMessage()); for (var element : throwable.getStackTraceElementProxyArray()){ stackTraceString.append("\n ") .append(element.getSTEAsString()); } request.put("StackTrace",stackTraceString); } String json = JSON.toJSONString(request); sendLogstash(json); } private String toLevelString(Level level) { if(level == Level.INFO) { return "Information"; }else if(level == Level.DEBUG) { return "Debug"; } else if (level == Level.ERROR) { return "Error"; } else if (level == Level.WARN) { return "Warning"; } return ""; } private void sendLogstash(String json){ if (!isStarted()) { return; } try { verifyResponse(logstash.addLog(json)); } catch (Exception ex) { addWarn("Can't execute PUT request. URL: '" + destination + "'", ex); } } @Override public synchronized void stop() { if (isStarted) { isStarted = false; } } @Override public boolean isStarted() { return isStarted; } @Override public String toString() { return destination == null ? "HttpClient" : "HttpClient --> " + destination; } public void setDestination(String destination) { this.destination = destination; } public void setUsername(String username){ this.username = username; } public void setPassword(String password){ this.password = password; } public void setTopic(String topic){ this.topic = topic; } public void setApp(String app){ this.app = app; } public void setLevel(String level){ this.level = level; } public List<Status> getStatusList() { return getStatusManager().getCopyOfStatusList(); } private void verifyResponse(Response response) throws IOException { if (response.status() != HttpURLConnection.HTTP_OK) { String msg = "" + "ResponseCode: " + response.status() + "; " + "Reason: " + response.reason() + "; " + "URL: " + response.request().url(); throw new IOException(msg); } } private boolean hasAnyErrors() { return getStatusList().stream().anyMatch(x -> x.getLevel() > 1); } }
HttpAppender代码:
package com.unicorn.tec.logstash; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import net.logstash.logback.encoder.LogstashEncoder; import org.springframework.stereotype.Component; @Component public class HttpAppender extends AppenderBase<ILoggingEvent> { private LogstashEncoder encoder; private HttpClient httpClient; @Override public synchronized void start(){ if(isStarted()){ return; } verifyConfigurationParameters(); addInfo("HttpAppender started"); super.start(); } @Override public void append(ILoggingEvent event) { if (!isStarted()) { return; } httpClient.addLog(event); } public void setEncoder(LogstashEncoder encoder) { this.encoder = encoder; } public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } private void verifyConfigurationParameters(){ if(encoder == null){ addError("No encoder was configured. Use <encoder> to specify the class name"); } if(httpClient == null){ addError("No destination was configured. Use <httpClient> to specify HttpClient"); } } }
至此,项目中所有error级别的log都会被推送到logstash中。但如果想要记录程序运行时的异常的话,则需要通过Spring框架截获异常并记录到logback中。
4、截获异常并记录
为了方便定位问题,我将request的一些信息也记录在日志里了。
@ControllerAdvice @Slf4j public class TecExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = {Exception.class}) protected Object handleExceptionInternal(Exceptionex, WebRequest request) { MDC.put("url", ((ServletWebRequest) request).getRequest().getRequestURI()); MDC.put("parameters",JSON.toJSONString(request.getParameterMap())); MDC.put("cookie",request.getHeader("cookie")); log.error(ex.getMessage(), ex); MDC.clear(); return ..... } }
5、总结
核心思路就是通过监听logback的ILoggingEvent事件,然后封装成自己想要的内容,发送到自己的logstash中。因为logback无法直接记录exception,所以需要截获exception并记录到log中(可能是自己没找到合适的方法,希望大神纠正)。
参考: