Dubbo过滤器 - AccessLogFilter

概述

AccessLogFilter 是一个日志过滤器,在服务提供端生效,主要用于记录服务每一次的请求日志。虽然 AccessLogFilter 默认会被激活,但还是需要手动配置来开启日志的打印。注意:此日志量比较大,请注意磁盘容量。

配置

  • 标签
    1
    2
    3
    <dubbo:protocol accesslog="xxx">
    <dubbo:provider accesslog="xxx">
    <dubbo:service accesslog="xxx">
  • 配置方式
    1. accesslog = “true” 或 accesslog=”default” : 向日志组件 Logger 中输出访问日志,如logbak,将日志输出到应用本身的 log 目录下。
    2. accesslog = “文件路径” :直接把访问日志输出到指定文件中。

日志打印规则:
如果配置的是将日志输出到日志组件,则立即写入。如果配置的是将日志输出到文件中,则将日志放入内存日志集合中,并开启定时任务进行日志持久化。

代码实现

属性

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
@Activate(group = Constants.PROVIDER, value = Constants.ACCESS_LOG_KEY)
public class AccessLogFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(AccessLogFilter.class);


//--------------- 使用日志组件输出相关属性 -------------------------/

/**
* 日志名前缀,用于获取日志组件。用于 accesslog = true,或 accesslog = default 的情况
*/
private static final String ACCESS_LOG_KEY = "dubbo.accesslog";


//--------------- 配置输出到指定文件的相关属性 ---------------------/
/**
* 日志的文件后缀
*/
private static final String FILE_DATE_FORMAT = "yyyyMMdd";
/**
* 时间格式化
*/
private static final String MESSAGE_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* 队列大小
*/
private static final int LOG_MAX_BUFFER = 5000;
/**
* 日志输出频率,单位:毫秒
*/
private static final long LOG_OUTPUT_INTERVAL = 5000;

/**
* 日志队列
* key: 自定的 accesslog 的值,如: accesslog="accesslog.log"
* value: 日志集合
*/
private final ConcurrentMap<String, Set<String>> logQueue = new ConcurrentHashMap<String, Set<String>>();
/**
* 定时任务线程池
*/
private final ScheduledExecutorService logScheduled = Executors.newScheduledThreadPool(2, new NamedThreadFactory("Dubbo-Access-Log", true));
/**
* 记录日志任务
*/
private volatile ScheduledFuture<?> logFuture = null;

// 省略其它代码
}

invoke 方法

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
+--- AccessLogFilter
@Override
public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
try {
// 记录访问日志的文件名
String accesslog = invoker.getUrl().getParameter(Constants.ACCESS_LOG_KEY);
if (ConfigUtils.isNotEmpty(accesslog)) {
// dubbo 上下文
RpcContext context = RpcContext.getContext();
// 服务名
String serviceName = invoker.getInterface().getName();
// 版本号
String version = invoker.getUrl().getParameter(Constants.VERSION_KEY);
// 分组
String group = invoker.getUrl().getParameter(Constants.GROUP_KEY);
// 拼接日志内容
StringBuilder sn = new StringBuilder();
sn.append("[")
// 时间
.append(new SimpleDateFormat(MESSAGE_DATE_FORMAT).format(new Date()))
// 调用方地址
.append("] ").append(context.getRemoteHost()).append(":").append(context.getRemotePort())
// 本地地址
.append(" -> ").append(context.getLocalHost()).append(":").append(context.getLocalPort())
.append(" - ");

// 分组
if (null != group && group.length() > 0) {
sn.append(group).append("/");
}
// 服务名
sn.append(serviceName);
// 版本
if (null != version && version.length() > 0) {
sn.append(":").append(version);
}
sn.append(" ");
// 方法名
sn.append(inv.getMethodName());
sn.append("(");
// 参数类型
Class<?>[] types = inv.getParameterTypes();
if (types != null && types.length > 0) {
boolean first = true;
for (Class<?> type : types) {
if (first) {
first = false;
} else {
sn.append(",");
}
sn.append(type.getName());
}
}
sn.append(") ");
// 参数值
Object[] args = inv.getArguments();
if (args != null && args.length > 0) {
sn.append(JSON.toJSONString(args));
}
// 日志信息字符串
String msg = sn.toString();
// 设置 accesslog = true 或 accesslog=default,将日志输出到日志组件Logger,如 logback中
if (ConfigUtils.isDefault(accesslog)) {
// 写日志
LoggerFactory.getLogger(ACCESS_LOG_KEY + "." + invoker.getInterface().getName()).info(msg);
} else {
// 异步输出到指定文件
log(accesslog, msg);
}
}
} catch (Throwable t) {
logger.warn("Exception in AcessLogFilter of service(" + invoker + " -> " + inv + ")", t);
}
return invoker.invoke(inv);
}

辅助方法

仅用于设置日志写入到文件的情况下。

写日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+--- AccessLogFilter
/**
* 添加日志内容到日志队列
*
* @param accesslog 日志路径
* @param logmessage 日志内容
*/
private void log(String accesslog, String logmessage) {
// 初始化任务
init();

// 获得日志队列
Set<String> logSet = logQueue.get(accesslog);
if (logSet == null) {
logQueue.putIfAbsent(accesslog, new ConcurrentHashSet<String>());
logSet = logQueue.get(accesslog);
}

// 若未超过队列大小,添加到队列中
if (logSet.size() < LOG_MAX_BUFFER) {
logSet.add(logmessage);
}
}

初始化任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+--- AccessLogFilter
/**
* 初始化任务
*/
private void init() {
// 双重检锁机制,防止重复初始化
if (logFuture == null) {
synchronized (logScheduled) {
if (logFuture == null) {
logFuture = logScheduled.scheduleWithFixedDelay(new LogTask(), LOG_OUTPUT_INTERVAL, LOG_OUTPUT_INTERVAL, TimeUnit.MILLISECONDS);
}
}
}
}

日志任务

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
+--- AccessLogFilter
/**
* 日志任务
*/
private class LogTask implements Runnable {
@Override
public void run() {
try {
if (logQueue != null && logQueue.size() > 0) {
// 遍历日志队列
for (Map.Entry<String, Set<String>> entry : logQueue.entrySet()) {
try {
// 获得日志文件路径
String accesslog = entry.getKey();
// 获得日志集合
Set<String> logSet = entry.getValue();
// 创建日志文件
File file = new File(accesslog);
File dir = file.getParentFile();
if (null != dir && !dir.exists()) {
dir.mkdirs();
}
if (logger.isDebugEnabled()) {
logger.debug("Append log to " + accesslog);
}
// 归档历史日志文件,例如: xxx.20191217
if (file.exists()) {
String now = new SimpleDateFormat(FILE_DATE_FORMAT).format(new Date());
String last = new SimpleDateFormat(FILE_DATE_FORMAT).format(new Date(file.lastModified()));
if (!now.equals(last)) {
File archive = new File(file.getAbsolutePath() + "." + last);
file.renameTo(archive);
}
}

// 输出日志到指定文件
FileWriter writer = new FileWriter(file, true);
try {
for (Iterator<String> iterator = logSet.iterator();
iterator.hasNext();
iterator.remove()) {

// 写入一行日志
writer.write(iterator.next());
// 换行
writer.write("\r\n");
}
// 刷盘
writer.flush();
} finally {
writer.close();
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}

小结

  1. AccessLogFilter 中会开启一个定时线程池,该线程池只有在指定了输出的文件时才会用到,该定时线程池会定时将队列中的日志写入文件中。
  2. 如果用户配置了使用应用本身的日志组件,则直接通过封装的 LoggerFactory 打印日志。如果用户配置了日志要输出到自定义的文件中,则会把日志加入到Map缓存中,key 是定义的 accesslog 的值,value 是对应的日志集合。后续等待定时线程不断遍历Map缓存,把日志写入到对应的文件中。
  3. 如果是日志输入到文件的情况,会有两个问题:
    • 由于Set集合是无序的,因此日志输出到文件也是无序的
    • 由于是异步刷盘,如果服务突然宕机会导致一部分日志丢失