所谓打点日志,是指用于数据统计的日志,一般前端会有打点的方法,而后端也会有类似需求。按理说,后端的一般的日志也可以做到打点的效果,但是因为一般的日志离散,风格不统一,得到的日志格式不好,不容易切分统计,所以就会想要一个独立的打点日志来实现打点。
实现打点日志的方法
- 通过日志模版,增加一种类型的打点日志,则替换模版中的占位符,重新生成一遍日志的配置文件,之后热更新日志配置。
- 使用日志框架API,不借助配置文件,直接通过编码的方式,生成logger
这两种各有好处,第一种打点日志实现和具体日志框架不绑定,且系统重启之后,配置不丢失,第二种实现简单,但是要使用具体日志框架的特定事先才行。
下面是一个使用日志框架API 打点日志的实现,代码如下,实际使用的时候,配合一个log.properties,从外部控制日志的一些配置。
下面打点日志,可以实现:
- 对于不同业务,增加一个业务枚举类,实现接口,不需要配置日志,即可将日志区分开,打到特定的目录下面
- 同一业务枚举下区分子类型,可以用来做数据统计、分类汇总等
- 日志使用统一的分隔符,格式统一,方便切分查询
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.RollingPolicy;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.util.FileSize;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* 打点日志基础实现的LoggerFactory
*
**/
public class BaseLoggerFactory {
private static final Properties LOG_PROPERTES = new Properties();
private static String APP_NAME;
private static String LOG_ROOT;
/**
* 日志内容格式
*/
private static String LOG_PATTERN;
/**
* 日志文件名格式
*/
private static String FILE_PATTERN;
/**
* 归档日志文件名格式
*/
private static String ROLLING_FILE_PATTERN;
private static int MAX_HISTORY;
private static String MAX_FILE_SIZE;
private static LoggerContext CONTEXT = (LoggerContext) LoggerFactory.getILoggerFactory();
private static Map<String, Logger> cacheLoggers = new ConcurrentHashMap<>();
static {
try {
LOG_PROPERTES.load(BaseLoggerFactory.class.getClassLoader().getResourceAsStream("log.properties"));
} catch (Exception e) {
//ignore
}
APP_NAME = LOG_PROPERTES.getProperty("appName", "TestApplication");
LOG_ROOT = System.getProperty("user.home") + "/" + APP_NAME + "/logs";
LOG_PATTERN = LOG_PROPERTES.getProperty("logPattern",
"%d{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%m%n");
FILE_PATTERN = LOG_PROPERTES.getProperty("filePattern",
LOG_ROOT + "/statistic/${logBizType}/${logBizType}.log");
ROLLING_FILE_PATTERN = LOG_PROPERTES.getProperty("rollingFilePattern",
LOG_ROOT + "/statistic/${logBizType}/${logBizType}.log.%d{yyyyMMdd}%i.gz");
MAX_HISTORY = Integer.parseInt(LOG_PROPERTES.getProperty("maxHistory", "15"));
MAX_FILE_SIZE = LOG_PROPERTES.getProperty("maxFileSize", "256MB");
}
static Logger getLogger(String name) {
// 使用双重检查锁,因为ConcurrentHashMap提供了读写操作的happens-before承诺,不会有双重检查锁的弊端
Logger logger = cacheLoggers.get(name);
if (logger != null) {
return logger;
}
synchronized (BaseLoggerFactory.class) {
logger = cacheLoggers.get(name);
if (logger != null) {
return logger;
}
logger = CONTEXT.getLogger(name);
logger.addAppender(generateAppender(CONTEXT, name));
logger.setLevel(Level.INFO);
logger.setAdditive(false);
logger = cacheLoggers.putIfAbsent(name, logger);
return logger;
}
}
private static Appender<ILoggingEvent> generateAppender(Context context, String logBizType) {
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
// 当前日志的文件命名格式
String filePatternInstance = FILE_PATTERN.replaceAll("\\$\\{logBizType}", logBizType);
appender.setFile(filePatternInstance);
appender.setContext(context);
appender.setEncoder(generateEncoder(context));
appender.setRollingPolicy(generateRollingPolicy(context, appender, logBizType));
//sizeAndTimeBasedRollingPolicy实现了TriggeringPolicy,不需要setTriggeringPolicy()
appender.start();
return appender;
}
private static RollingPolicy generateRollingPolicy(Context context, FileAppender<ILoggingEvent> fileAppender, String logBizType) {
SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new SizeAndTimeBasedRollingPolicy<>();
rollingPolicy.setContext(context);
rollingPolicy.setParent(fileAppender);
// 归档文件的命名格式
String rollingFilePatternInstance = ROLLING_FILE_PATTERN.replaceAll("\\$\\{logBizType}", logBizType);
rollingPolicy.setFileNamePattern(rollingFilePatternInstance);
rollingPolicy.setMaxHistory(MAX_HISTORY);
rollingPolicy.setMaxFileSize(FileSize.valueOf(MAX_FILE_SIZE));
rollingPolicy.start();
return rollingPolicy;
}
private static Encoder<ILoggingEvent> generateEncoder(Context context) {
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setCharset(Charset.forName("UTF-8"));
encoder.setContext(context);
// 日志内容的格式
encoder.setPattern(LOG_PATTERN);
encoder.start();
return encoder;
}
}
import ch.qos.logback.classic.Logger;
/**
* 打点日志
* 通过枚举类,可以统计实现不同枚举业务日志分文件统计打点数据
*
**/
public class StatisticLogger {
private static final String DELIMITER = "|";
public static void statisticLog(ILogBizType logBizType, ISubLogBizType statisticType, Object... args) {
Logger logger = BaseLoggerFactory.getLogger(logBizType.getCode());
// 打点日志从需求上说不需要堆栈信息,所以没有涉及堆栈的获取
StringBuilder msg = new StringBuilder()
.append(statisticType.getSubType());
// 为了处理可变参数内容是数组的情况
for (int i = 0; i < args.length; i++) {
msg.append(DELIMITER).append("{}");
}
logger.info(msg.toString(), args);
}
}
/**
* 打点日志使用的业务类型
* 可以继承此接口实现自己的枚举类来枚举不同业务
*
**/
public interface ILogBizType {
/**
* 返回一个表示业务类型的code,打点日志的目录名和日志名均依据此命名
* @return
*/
String getCode();
}
/**
* logBizType的细分类型
*
* 通过扩展该类实现枚举类,实现区分LogBizType下的不同场景
**/
@FunctionalInterface
public interface ISubLogBizType {
/**
* 子类型,用于区分一个LogBizType下的不同场景使用
* @return
*/
int getSubType();
}
/**
* 打点日志的枚举类
* 可以继承它拓展自己的业务枚举类
**/
public enum LogBizType implements ILogBizType{
/**
* 错误日志类型
*/
ERROR("error"),
/**
* 默认日志类型
*/
DEFAULT("default"),
/**
* 系统日志类型
*/
SYS("system");
LogBizType(String code){
this.code = code;
}
private String code;
@Override
public String getCode() {
return code;
}
}