zuul+springboot+db+elk实现访问日志低延迟保存及可视化分析

做java的,一般小项目都是使用logback配置控制台、文件等对日志进行记录和持久化,但是随着微服务架构的越来越流行,各种微服务模块越来越多,节点也越来越多,线上日志的排查和访问统计也跟着变的更复杂和不便。

于是想着研究有什么更好的架构方案,网上找了一圈,质量参差不齐,但得到一个结论是,用elk(即elastic+logstash+kibana)是现在比较流行的方法。研究了一下,觉得应该不错,于是便着手开搞。

先介绍一下以前的流程,由于本文主要介绍api接口访问日志的保存和分析,故后台的系统日志就不做介绍了。

老版访问日志处理流程

上面这张图介绍的是老版本的流程,该系统是众多微服务之一,并且未架设网关。主要有几个问题:

1.未做限流处理,可能导致redis崩掉
2.及时性不够高,因为定时任务间隔较短
3.可能导致db cpu100%,因为db只有一个节点,所有读写、后台的操作都在此上进行
4.扩展性不好,此时如果需要记录别的微服务,得再来一遍
5.每新增一个新的维度数据分析统计,需要前后端配合开发进行可视化,效率低

为了解决上述问题,尝试了几种方案。

方案1:

logback配置直接输出到logstash,再通过logstash输出到es,再通过kibana展示。然后由于日志需要长期保存,为免es出现问题(es才用不久,了解不深),将es中数据再同步到db。

遇到问题:

log.info(RequestModel)这样打日志,RequestModel中的信息会在logstash中单独以一个"message"字段进行保存,这对后面kibana写过滤表达式很难处理。后来采取java代码中"log.info("{} {} {}", request_uri, request_ip, request_param…)"这种日志产生后,仅希望将这些request_*字段输出至logstash,多余的字段不要,可用grok过滤,但一些自定义的字段不太会处理,正则表达式也不怎么会用,最终放弃。(其实是想到了别的方案)

方案2:

既然日志需要保存到db,那么logback配置就改为输出到db,再把数据传到es,再通过kibana展示。

遇到问题:仍然是关于仅保存自定义字段的需求,网上了解了一圈,默认是要3张表,肯定不是我要的;还有用继承重写的,但看起来仍然没有达到我的要求。并且还有一个连接池的问题没有解决,就也没有研究下去。

方案3:

既然日志需要自定义字段,这些中间件我又用不溜,那就自己处理吧。获取到请求信息以后,手动持久化到db,再把数据传到es,再通过kibana展示。

经过一番折腾,最终成功实现了方案3。

也介绍一下新的架构:
新版访问日志处理流程

改造关键点如下:

1.最前端架设了一道网关,所有的请求都通过网关进行转发,这样在网关层,就能统一实现限流、鉴权、熔断等,当然包括本文讲的日志。
2.日志获取处理完成后,在中间加一道阻塞式队列,用以保存日志和缓冲,要注意线程有关的配置和同步。
3.db脱离主业务db,专门另开一个db用以保存,且日志表不带任何索引(除主键)。

下面上主要的代码:

基础过滤类

package com.suitwin.gateway.zuul;
import java.util.ArrayList;
import java.util.List;
import com.netflix.zuul.ZuulFilter;
/**
 * @desc   抽象基础过滤,放置常量,供自定义的两种过滤器集成共用
 * @auth   kerry.wang
 * @date   2021年4月18日
 */
public abstract class ZuulFilterBase extends ZuulFilter {
	protected static final String KEY_BEFOREFILTERRESPONSE = "beforeFilterResponse";
	protected static final String KEY_REQS = "reqs";
	protected static final String UTF8 = "UTF-8";
	protected static final String CONTENTTYPE = "Content-type";
	protected static final String APPLICATIONJSON = "application/json;charset=utf-8";
	
	// 需要过滤做日志的微服务名称列表
	protected final static List<String> SERVICELIST = new ArrayList<String>(7) {
		private static final long serialVersionUID = -1153328974427768155L;
	{
		add("suitwin-epcv31");
		add("suitwin-epc");
		add("suitwin-amc");
		add("suitwin-bmc");
		add("suitwin-dmc");
		add("suitwin-mms");
		add("suitwin-trace");
	}};
}

网关转发的预过滤

package com.suitwin.gateway.zuul;
import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.suitwin.common.constant.ConstResponse;
import com.suitwin.common.util.RedisUtil;
import com.suitwin.common.util.StringUtil;
import com.suitwin.gateway.util.JwtUtil;
/**
 * @desc   自定义网关过滤器,转发前的过滤
 * @auth   kerry.wang
 * @date   2019年8月8日
 */
@Component
public class ZuulFiltersBefore extends ZuulFilterBase {
	@Resource(name="gatewayJwt")
	private JwtUtil jwt;
	
	@Override
	public boolean shouldFilter() {
		return true;// 设置开启过滤
	}
	/**
	 * 这里做令牌验证
	 */
	@Override
	public Object run() throws ZuulException {
		RequestContext ctx = RequestContext.getCurrentContext();
		
		// 保存请求的时间戳
		ctx.set(KEY_REQS, System.currentTimeMillis());
		
		// 如果需要令牌,拦截并校验
		if(tokenRequired(ctx)) {
			if(validToken(ctx)) {
				ctx.setSendZuulResponse(true);
			}
		}else {
			ctx.setSendZuulResponse(true);
		}
		
		return null;
	}
	
	@Override
	public String filterType() {
		return FilterConstants.PRE_TYPE;// 代表在路由转发之前进行过滤
	}
	@Override
	public int filterOrder() {	
		return 0;// 值越小,优先级越高
	}
	
	/**
	 * @desc   验证uri是否需要令牌校验
	 * @auth   kerry.wang
	 * @date   2021年4月9日
	 * @param  request_uri
	 * @return
	 */
	private boolean tokenRequired(RequestContext ctx) {
		String request_uri = ctx.getRequest().getRequestURI();
		for(String ignoreUri : ExcludePath.excludePathList) {
			if(request_uri.startsWith(ignoreUri) || request_uri.endsWith(ignoreUri)) {
				return false;
			}
		}
		return true;
	}
	/**
	 * @desc   验证令牌
	 * @auth   kerry.wang
	 * @date   2021年4月9日
	 * @param  request
	 */
	private boolean validToken(RequestContext ctx) {
		HttpServletRequest request = ctx.getRequest();
		String token = request.getHeader("token");
		String requestUri = request.getRequestURI();
		// token为空
        if (StringUtil.isEmpty(token)) {
        	return interceptRouter(ctx, ConstResponse.TOKEN_EMPTY.code(), ConstResponse.TOKEN_EMPTY.des(), requestUri);
        }
        // token过期
        String currentUser = RedisUtil.get(token);
        if (StringUtil.isEmpty(currentUser)) {
            return interceptRouter(ctx, ConstResponse.TOKEN_EXPIRED.code(), ConstResponse.TOKEN_EXPIRED.des(), requestUri);
        }
        String username = JwtUtil.getUsername(token);
        Integer companyID = JwtUtil.getCompanyID(token);
        
        // 验证令牌是否有效
        boolean verifyToken = companyID == null ? jwt.verify(token, username) : jwt.verify(token, username, companyID);
        if (!verifyToken) {
            return interceptRouter(ctx, ConstResponse.TOKEN_INVALID.code(), ConstResponse.TOKEN_INVALID.des(), requestUri);
        }
        
        if(StringUtil.notEmpty(username)) {
        	ctx.set("username", username);// 保存请求的用户
        }
        if(companyID != null) {
        	ctx.set("companyID", companyID);// 保存请求的公司
        }
		
		String requestFrom = JwtUtil.getPlatform(token);
		if(StringUtil.notEmpty(requestFrom)) {
			ctx.set("requestFrom", requestFrom);
		}
        return true;
	}
	
	/**
	 * @desc   拦截路由
	 * @auth   kerry.wang
	 * @date   2021年4月9日
	 * @param  ctx
	 * @param  code
	 * @param  msg
	 * @param  requestUri
	 */
	private boolean interceptRouter(RequestContext ctx, int code, String msg, String requestUri) {
		String respJson = "{\"code\":" + code + ", \"msg\":\"" + requestUri + " => " + msg + "\", \"data\":null}";
		// 在转发前被截断,做好标记,这样在转发后的拦截器中,就不再设置响应数据
		ctx.set(KEY_BEFOREFILTERRESPONSE, respJson);
		// 设置是否对请求进行路由
		ctx.setSendZuulResponse(false);
		ctx.setResponseStatusCode(200);
		try {
			ctx.getResponse().addHeader(CONTENTTYPE, APPLICATIONJSON);
			ctx.getResponse().getWriter().write(respJson);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}
	
	
}

网关转发响应后的过滤

package com.suitwin.gateway.zuul;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.suitwin.common.util.IPUtil;
import com.suitwin.common.util.StringUtil;
import com.suitwin.common.util.TimeUtil;
import com.suitwin.gateway.model.AccessLog;
import com.suitwin.gateway.service.ILogService;
/**
 * @desc   网关过滤器,获取转发后的响应之前的过滤
 * @auth   kerry.wang
 * @date   2021年4月13日
 */
@Component
public class ZuulFiltersAfter extends ZuulFilterBase {
	@Autowired
	private RouteLocator routeLocator;
	@Resource(name="dblog")
	private ILogService dbLog;
	
	@Override
	public boolean shouldFilter() {
		return true;// 设置开启过滤
	}
	
	@Override
	public Object run() throws ZuulException {
		// 获取转发请求后的响应数据
		RequestContext ctx = RequestContext.getCurrentContext();
		InputStream stream = ctx.getResponseDataStream();
		String body = null;
		try {
			body = StreamUtils.copyToString(stream, Charset.forName(UTF8));
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		AccessLog accessLog = parseRequestInfo(ctx);
		
		if(accessLog != null) {
			// 是否已被ZuulFiltersBefore拦截并处理响应
			boolean hasInterceptedByBeforeFilter = ctx.containsKey(KEY_BEFOREFILTERRESPONSE);
			
			// 异步记录访问日志到logstash
//			logstashLog.doLog(log, body);
			// 异步塞入log队列
			dbLog.doLog(accessLog, hasInterceptedByBeforeFilter ? String.valueOf(ctx.get(KEY_BEFOREFILTERRESPONSE)) : body);
			
			// 通过前面转发前的拦截,对ctx是否配置了sendZuulResponse判断是否令牌验证正常。如果这里不加这段处理,ctx.setResponseBody将出错,response.getWriter has called... 
			if(hasInterceptedByBeforeFilter) {
				return null;
			}
		}
		
		// 将转发后的响应set回网关对请求的响应,不set的话,本次请求将没有任何相应数据
		ctx.setResponseBody(body);
		return null;
	}
	@Override
	public String filterType() {
		return FilterConstants.POST_TYPE;// 代表在路由转发并响应后进行过滤
	}
	@Override
	public int filterOrder() {
		return 0;
	}
	
	/**
	 * @desc   解析请求信息获得请求日志
	 * @auth   kerry.wang
	 * @date   2021年4月12日
	 * @param  ctx
	 * @return
	 */
	private AccessLog parseRequestInfo(RequestContext ctx) {
		HttpServletRequest req = ctx.getRequest();
		// 获取转发到的微服务名称
		Route route = routeLocator.getMatchingRoute(req.getRequestURI());
		if(!doFilter(route.getLocation())) {
			return null;
		}
		
		// ip位置信息
		String ip = IPUtil.getIpAddr(req);
		String country = "", province = "", city = "";
		if(StringUtil.notEmpty(ip)) {
			String ipInfo = IPUtil.getIpInfo(ip);
			if(StringUtil.notEmpty(ipInfo) && ipInfo.contains("|")) {
				// 中国|0|江苏|无锡|电信
				country = ipInfo.split("\\|")[0];
				province = ipInfo.split("\\|")[2];
				city = ipInfo.split("\\|")[3];
			}
		}
		// 请求耗时
		Long requestCost = System.currentTimeMillis() - Long.valueOf(String.valueOf(ctx.get(KEY_REQS)));
		
		AccessLog accessLog = new AccessLog();
		accessLog.setRequestUri(req.getRequestURI());
		accessLog.setRequestUrl(req.getRequestURL().toString());
		accessLog.setRequestIp(ip);
		accessLog.setRequestCountry(country);
		accessLog.setRequestProvince(province);
		accessLog.setRequestCity(city);
		accessLog.setRequestType(req.getMethod());
		accessLog.setRequestTime(TimeUtil.getCurrentDatetime_safety());
		accessLog.setRequestCost(requestCost);
		accessLog.setRequestService(route.getLocation());
		
		// 获取请求体参数
		try {
			InputStream in = req.getInputStream();
			String reqbody = StreamUtils.copyToString(in, Charset.forName(UTF8));
			if(StringUtil.notEmpty(reqbody)) {
				accessLog.setRequestParamBoby(reqbody);
			}
		}catch (Exception e) {
			e.printStackTrace();
		}
		
		// 获取请求url上的参数
		String queryString = req.getQueryString();
		if(StringUtil.notEmpty(queryString)) {
			accessLog.setRequestParamQuery(queryString);
		}
			
		if(ctx.containsKey("username")) {
			accessLog.setRequestUser(String.valueOf(ctx.get("username")));
		}
		if(ctx.containsKey("companyID")) {
			accessLog.setRequestCompany(String.valueOf(ctx.get("companyID")));
		}
		if(ctx.containsKey("requestFrom")) {
			accessLog.setRequestFrom(String.valueOf(ctx.get("requestFrom")));
		}
		return accessLog;
	}
	
	/**
	 * @desc   判断是否是需要过滤的微服务
	 * @auth   kerry.wang
	 * @date   2021年4月16日
	 * @param  service
	 * @return
	 */
	private boolean doFilter(String service) {
		return SERVICELIST.contains(service);
	}
	
}

日志业务处理保存

package com.suitwin.gateway.log;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.suitwin.common.util.StringUtil;
import com.suitwin.gateway.model.AccessLog;
import com.suitwin.gateway.service.ILogService;
import lombok.extern.slf4j.Slf4j;
@Component("dblog")
@Slf4j
public class DBLog implements ILogService {
	@Async("logExecutor")
	@Override
	public void doLog(AccessLog accessLog, String responseJson) {
		accessLog = convertRequestCompanyInfo(accessLog);
		accessLog = parseResponseInfo(accessLog, responseJson);
		try {
			LogTask.LOGQUEUE.put(accessLog);
		} catch (InterruptedException e) {
			log.error("===>>> logqueue put failed, " + e.getMessage());
		}
	}
	
	/**
	 * @desc   将请求中保存的公司id转换为公司名称
	 * @auth   kerry.wang
	 * @date   2021年4月13日
	 * @param  accessLog
	 * @return
	 */
	private AccessLog convertRequestCompanyInfo(AccessLog accessLog) {
		if(StringUtil.notEmpty(accessLog.getRequestUser())) {
			
		}
		if(StringUtil.notEmpty(accessLog.getRequestCompany())) {
			// 从db中根据公司id读取出公司名
//			accessLog.setRequestCompany(requestCompany);
			// 读取出公司所属行业
//			accessLog.setRequestUserIndustry(requestUserIndustry);
		}
		return accessLog;
	}
	/**
	 * @desc   解析响应信息
	 * @auth   kerry.wang
	 * @date   2021年4月13日
	 * @param  accessLog
	 * @param  responseJson
	 * @return
	 */
	private AccessLog parseResponseInfo(AccessLog accessLog, String responseJson) {
		if(StringUtil.notEmpty(responseJson)) {
			JSONObject jo = JSONObject.parseObject(responseJson);
			Integer requestResultcode = jo.getInteger("code");
			accessLog.setRequestResultcode(requestResultcode);
			if(requestResultcode == 100) {
				// 对vin、点选、oe搜索和零件关键词搜索接口做拦截,提取出其中的车辆有关信息
				String uri = accessLog.getRequestUri();
				
				if(uri.contains("parseVin/v")) {
					jo = JSONObject.parseObject(jo.getString("data"));
					accessLog.setRequestBrand(jo.getString("brand"));
					accessLog.setRequestFactory(jo.getString("factory"));
					accessLog.setRequestSeries(jo.getString("series"));
				}
				
				else if(uri.contains("getEpcModel/v")) {
					
				}
				
				else if(uri.contains("searchOe/v")) {
					
				}
				
				else if(uri.contains("xxxxx")) {
					
				}
			}
		}
		return accessLog;
		
		
		// 记点,存入nosql
	}
	
}

log持久化任务

package com.suitwin.gateway.log;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.suitwin.gateway.dao2.LogDao;
import com.suitwin.gateway.model.AccessLog;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class LogTask {
	private LogTask() {};
	// log队列
	public static BlockingQueue<AccessLog> LOGQUEUE = new LinkedBlockingQueue<AccessLog>(5000);
	// 待插入log集合
	private volatile List<AccessLog> LOGLIST = new CopyOnWriteArrayList<AccessLog>();
	@Autowired
	private LogDao logDao;
	// 最后一次日志插入时间
	private long lastLogTimeMillions = 0;
	// 定时任务间隔时间
	private final static int CHECKINTEVAL = 5 * 1000;
	// 空闲时间
	private final static int FREETIME = 30 * 1000;
	// 批量插入大小
	private final static int BATCHSIZE = 1000;
	
	/**
	 * @desc   创建日志队列的拉取任务,并生成检测器,单线程
	 * @auth   kerry.wang
	 * @date   2021年4月16日
	 */
	public void createLogJob() {
		checkQueue();
		log.info("===>>> Log job started...");
		while(true) {
			while(!LOGQUEUE.isEmpty()) {
				log.debug("===>>> detect queue logs, start to poll...");
				synchronized (LOGLIST) {
					LOGLIST.add(LOGQUEUE.poll());
					if(LOGLIST.size() > BATCHSIZE) {
						int rs = logDao.insertAccessLogBatch(LOGLIST);
						lastLogTimeMillions = System.currentTimeMillis();
						LOGLIST.clear();
						log.debug("===>>> createLogJob 批量插入了 " + rs);
					}
				}
			}
		}
	}
	
	/**
	 * @desc   任务队列空闲检查,5s检查一次,空闲超过5秒,将剩余待插入log集合写入db
	 * @auth   kerry.wang
	 * @date   2021年4月16日
	 */
	private void checkQueue() {
		log.info("===>>> Queue check timer started...");
		new Timer() {{
        	schedule(new TimerTask() {
        		@Override
        		public void run() {
        			log.debug("===>>> queue checking...");
        			// 如果日志插入线程空闲大于5s,则将队列中的剩余日志批量插入db
        			if(System.currentTimeMillis() - lastLogTimeMillions > FREETIME) {
        				// LOGLIST还要加一道锁,避免和Log job产生差异
        				synchronized (LOGLIST) {
        					if(!LOGLIST.isEmpty()) {
        						log.debug("===>>> detect remain logs, start to write...");
        						int rs = logDao.insertAccessLogBatch(LOGLIST);
        						LOGLIST.clear();
        						log.debug("===>>> checkQueue 批量插入了 " + rs);
        					}
        				}
        			}
        		}
        	}, 0, CHECKINTEVAL);
        }};
	}
	
}

塞入日志队列的异步线程池配置

package com.suitwin.gateway.config;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
 * @desc   配置spring boot @async异步注解的线程池
 * @auth   kerry.wang
 * @date   2021年1月22日
 * spring boot 2.0默认线程池每开启一个新的任务都是新开一个线程的。2.2默认线程池有8个
 */
@Configuration
public class AsyncConfig {
	/**
	 * @desc   为同步日志到队列创建最大16线程处理池
	 * @auth   kerry.wang
	 * @date   2021年1月22日
	 * @return
	 */
	@Bean(name = "logExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(8);
        // 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(16);
        // 队列最大长度
        executor.setQueueCapacity(5000);
        // 允许的空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(100);
        // 线程前缀
        executor.setThreadNamePrefix("logExecutorThread-");
        // 这里使用第4种,保证一旦出现超过并发的上限时,依旧能够再次重试,否则会导致最终业务逻辑或数据不达预期
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
	
}

启动类

package com.suitwin.gateway;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import com.suitwin.gateway.log.LogTask;
@EnableZuulProxy
//@SpringBootApplication// (exclude= {DruidDataSourceAutoConfigure.class, DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.suitwin.common", "com.suitwin.gateway"})
@EnableAsync
public class SuitwinGatewayApplication implements CommandLineRunner {
	@Autowired
	private LogTask task;
	
	public static void main(String[] args) {
		SpringApplication.run(SuitwinGatewayApplication.class, args);
	}
	
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", addcorsConfig());
        return new CorsFilter(source);
    }
	
    private CorsConfiguration addcorsConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        List<String> list = new ArrayList<>();
        list.add("*");
        corsConfiguration.setAllowedOrigins(list);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(168000l);
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
	@Override
	public void run(String... args) throws Exception {
		System.err.println("===>>> Suitwin gateway started..."  +Thread.currentThread().getName());
		task.createLogJob();
	}
}

下面是logstash的关键配置:

input {
	jdbc {
		jdbc_connection_string => "jdbc:mysql://192.168.0.234:3307/epc_cache?useUnicode=true&characterEncoding=UTF-8"
		jdbc_driver_library => "D:/mysql-connector-java-5.1.35.jar"
		jdbc_driver_class => "com.mysql.jdbc.Driver"
		jdbc_user => "root"
		jdbc_password => "root"
		schedule => "* * * * *"
		statement => "SELECT * FROM epc_cache.stw_gateway_log WHERE id > :sql_last_value"
		use_column_value => true
		tracking_column => "id"
		last_run_metadata_path => "D:/logstash_log.txt"
		clean_run => false
	}
}
output {
	elasticsearch {
		hosts => ["http://192.168.0.234:9200"]
		index => "suitwin-gateway-log"
		document_type => "weblog"
		document_id => "%{id}"
	}
	stdout {
		codec => json_lines
	}
}

以上一套下来,启动spring boot、logstash、es、mysql,即可实现日志保存到mysql和es

最后数据可视化:

打开kibana,先通过创建索引,实现目标日志表的基础查看
基础日志查看

如果想要通过kibana实现后台日志的查看(而非登录服务器查看日志文件),选用上面介绍过的方案1即可,很方便。

ok,此时要进行分析了,可以通过filter过滤器,输入表达式,即可得到想要的结果,例如图中筛选了返回响应代码为106的日志。

那么如何构建图表,此时需要通过Visualize菜单进行创建。
可视化后的效果

上图展示了访问日志中,请求的汽车品牌的饼图统计。

ok,那么最终的各种树状图、折线图、饼图、热力图等等大屏数据展示如何完成?通过dashboard菜单。
仪表板效果

通过添加上一步创建的Visualize组件(需要保存),即可将一个个单独的图表聚合到一个仪表板中进行显示,并且可以任意调节组件大小。

到此为止,整个流程就ok了,将其中的细节打磨一下,就能实现系统的所有日志处理,最终无需技术人员的介入,即可轻松完成平台访问情况的检测、统计、分析、可视化!

本方案的不足之处还请不吝赐教 – 来自一个什么都不会搞的产品经理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值