0%

log4j2通过socket发送日志到elk平台

前言

尝试过很多 spring cloud中日志的方案 好多还是需要 集群的支持
但是现在公司还没有那么吊的基础设施 那么 这个时候 就需要 项目能够自己直接通过tcp或者udp直接投递日志到elk或者生成日志文件去采集了
但是 项目是使用docker 去部署在swarm或者k8s中 这个时候 生成日志文件 相对来说有点扯淡
但是公司的swarm集群又没有办法采集到标准输出和错误输出 那么就需要项目自己去投递日志到elk或者队列中让elk去接受了
由于时间较为紧急 直接采用log4j2的socketAppender 来投递日志 使用自定义jsonLayout去格式化 并且适配logstash

实践

编写jsonLayout
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package com.ming.log;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.*;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.layout.PatternSelector;
import org.apache.logging.log4j.core.pattern.RegexReplacement;

import java.io.File;
import java.nio.charset.Charset;

/**
* boss json格式日志
* <p>
* 配合elk的配置 使用
* 原作者文章地址: https://blog.csdn.net/lnkToKing/article/details/79563460
*
* @author ming
* @date 2018-06-22 10:59:56
*/
@Plugin(name = "MingJsonPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class MingJsonPatternLayout extends AbstractStringLayout {
/**
* 项目路径
*/
private static String PROJECT_PATH;

private PatternLayout patternLayout;

private String projectName;
private String logType;

static {
PROJECT_PATH = new File("").getAbsolutePath();
}

private BossJsonPatternLayout(Configuration config, RegexReplacement replace, String eventPattern,
PatternSelector patternSelector, Charset charset, boolean alwaysWriteExceptions,
boolean noConsoleNoAnsi, String headerPattern, String footerPattern, String projectName, String logType) {
super(config, charset,
PatternLayout.createSerializer(config, replace, headerPattern, null, patternSelector, alwaysWriteExceptions,
noConsoleNoAnsi),
PatternLayout.createSerializer(config, replace, footerPattern, null, patternSelector, alwaysWriteExceptions,
noConsoleNoAnsi));

this.projectName = projectName;
this.logType = logType;
this.patternLayout = PatternLayout.newBuilder()
.withPattern(eventPattern)
.withPatternSelector(patternSelector)
.withConfiguration(config)
.withRegexReplacement(replace)
.withCharset(charset)
.withAlwaysWriteExceptions(alwaysWriteExceptions)
.withNoConsoleNoAnsi(noConsoleNoAnsi)
.withHeader(headerPattern)
.withFooter(footerPattern)
.build();
}

@Override
public String toSerializable(LogEvent event) {
//在这里处理日志内容
String message = patternLayout.toSerializable(event);
String jsonStr = new JsonLoggerInfo(projectName, message, event.getLevel().name(), logType, event.getTimeMillis()).toString();
return jsonStr + "\n";
}

@PluginFactory
public static BossJsonPatternLayout createLayout(
@PluginAttribute(value = "pattern", defaultString = PatternLayout.DEFAULT_CONVERSION_PATTERN) final String pattern,
@PluginElement("PatternSelector") final PatternSelector patternSelector,
@PluginConfiguration final Configuration config,
@PluginElement("Replace") final RegexReplacement replace,
// LOG4J2-783 use platform default by default, so do not specify defaultString for charset
@PluginAttribute(value = "charset") final Charset charset,
@PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) final boolean alwaysWriteExceptions,
@PluginAttribute(value = "noConsoleNoAnsi", defaultBoolean = false) final boolean noConsoleNoAnsi,
@PluginAttribute("header") final String headerPattern,
@PluginAttribute("footer") final String footerPattern,
@PluginAttribute("projectName") final String projectName,
@PluginAttribute("logType") final String logType) {


return new BossJsonPatternLayout(config, replace, pattern, patternSelector, charset,
alwaysWriteExceptions, noConsoleNoAnsi, headerPattern, footerPattern, projectName, logType);
}

/**
* 输出的日志内容
*/
public static class JsonLoggerInfo {
/**
* 项目名
*/
private String projectName;
/**
* 项目目录路径
*/
private String projectPath;
/**
* 日志信息
*/
private String message;
/**
* 日志级别
*/
private String level;
/**
* 日志分类
*/
private String logType;
/**
* 日志时间
*/
private String time;

public JsonLoggerInfo(String projectName, String message, String level, String logType, long timeMillis) {
this.projectName = projectName;
this.projectPath = PROJECT_PATH;
this.message = message;
this.level = level;
this.logType = logType;
this.time = DateFormatUtils.format(timeMillis, "yyyy-MM-dd HH:mm:ss.SSS");
}

public String getProjectName() {
return projectName;
}

public String getProjectPath() {
return projectPath;
}

public String getMessage() {
return message;
}

public String getLevel() {
return level;
}

public String getLogType() {
return logType;
}

public String getTime() {
return time;
}

@Override
public String toString() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
}

配置log4j2.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
26
27
<?xml version="1.0" encoding="UTF-8"?>
<!-- monitorInterval="60"表示每60秒配置文件会动态加载一次。在程序运行过程中,如果修改配置文件,程序会随之改变。 -->
<configuration status="warn" monitorInterval="1">
<!-- 定义通用的属性 -->
<Properties>
<Property name="PROJECT_NAME">ming</Property>
<Property name="ELK_LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss} %-5p thread[%thread] %l %msg %n</Property>
</Properties>

<appenders>
<!--测试环境 elk的logstash 入口-->
<Socket name="logstash" host="<logstash-ip>" port="<logstash-port>" protocol="TCP">
<MingJsonPatternLayout pattern="${ELK_LOG_PATTERN}" projectName="${PROJECT_NAME}" logType="ming" />
</Socket>

</appenders>


<Loggers>

<!-- 配置项目的 日志等级输出 -->
<root level="DEBUG">
<!-- 通过tcp 传输到logstash-->
<appender-ref ref="logstash"/>
</root>
</Loggers>
</configuration>

替换xml中 为自己的 logstash ip和port

配置 logstash
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
input {
#开启远程输入日志服务
tcp {
port => "<logstash-port>"
mode => "server"
type => "log4j2"
}
}

filter {
#将日志转成json对象
json {
source => "message"
}
#将远程客户端的日志时间设置为插入时间,不设置默认为当前系统时间,可能会存在时间误差
date {
match => ["time", "yyyy-MM-dd HH:mm:ss.SSS"]
remove_field => ["time"]
}
}

output {
elasticsearch {
hosts => ["<es-ip>:<es-port>"]
index => "application-%{+YYYY.MM.dd}"
}
}

替换上面配置中的(和log4j2中port保持一致)、(默认为9200)
修改后重启logstash 启动配置 即可

总结

其实最终是期望 项目直接输出标准输出和错误输出 由swarm或者k8s直接统一采集 标准输出和错误输出 这样 又避免了生成实际文件 有简化了项目的配置
这个直接使用socketAppender投递日志存在缺陷 一是socket比较简略 如果追求高性能需要自己重写 socketAppender 二个 需要logstash的解析和log4j2中的Layout进行匹配
否则 输出的日志 一坨翔