Skip to content

Commit c83020b

Browse files
committed
Fix memory leak issues causing OOM after prolonged operation
This commit addresses GitHub issue #61 where memory usage keeps increasing and garbage collection fails, eventually causing node crashes after several days. Root Causes: 1. SimpleDateFormat objects were created on every message send operation (expensive and thread-unsafe, causing significant memory pressure) 2. HashMap and StringBuilder objects were allocated on every message (constant allocations under high throughput causing GC pressure) Changes Made: PlainSender.java: - Added ThreadLocal<SimpleDateFormat> to reuse date formatter instances - Added ThreadLocal<StringBuilder> to reuse string builder instances CEFSender.java: - Added ThreadLocal<StringBuilder> to reuse string builder (512 byte capacity) FullSender.java: - Added ThreadLocal<HashMap> instances for structured data parameters - Added ThreadLocal<StringBuilder> for dumpMessage method - All maps are cleared and reused instead of creating new instances StructuredSender.java: - Added ThreadLocal<HashMap> instances for structured data parameters - Maps are cleared and reused on each message TransparentSyslogSender.java: - Added ThreadLocal<SimpleDateFormat> to reuse date formatter - Added ThreadLocal<StringBuilder> to reuse string builder (256 byte capacity) SnareWindowsSender.java: - Added ThreadLocal<SimpleDateFormat> for both syslog and MS Event formats - Added ThreadLocal<StringBuilder> to reuse string builder (512 byte capacity) Impact: These changes significantly reduce object allocations and GC pressure by reusing thread-local instances instead of creating new objects for every message. This is especially critical under high message throughput where the plugin was previously creating millions of short-lived objects. The ThreadLocal pattern ensures thread-safety while maintaining performance and dramatically reducing memory consumption. Fixes #61 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7401ea5 commit c83020b

File tree

6 files changed

+187
-42
lines changed

6 files changed

+187
-42
lines changed

src/main/java/com/wizecore/graylog2/plugin/CEFSender.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
/*
1414
* http://blog.rootshell.be/2011/05/11/ossec-speaks-arcsight/
15-
*
16-
*
15+
*
16+
*
1717
* CEF:Version|Device Vendor|Device Product|Device Version|Signature ID|Name|Severity|Extension
1818
1919
CEF:0|ArcSight|Logger|5.0.0.5355.2|sensor:115|Logger Internal Event|1|\
@@ -23,9 +23,20 @@
2323
*/
2424
public class CEFSender implements MessageSender {
2525

26+
/**
27+
* ThreadLocal StringBuilder to avoid allocating new StringBuilder on every message
28+
*/
29+
private static final ThreadLocal<StringBuilder> STRING_BUILDER_CACHE = new ThreadLocal<StringBuilder>() {
30+
@Override
31+
protected StringBuilder initialValue() {
32+
return new StringBuilder(512);
33+
}
34+
};
35+
2636
@Override
2737
public void send(SyslogIF syslog, int level, Message msg) {
28-
StringBuilder out = new StringBuilder();
38+
StringBuilder out = STRING_BUILDER_CACHE.get();
39+
out.setLength(0);
2940

3041
// Header:
3142
// CEF:Version|Device Vendor|Device Product|Device Version|

src/main/java/com/wizecore/graylog2/plugin/FullSender.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
/**
1515
* Sends full message to Syslog.
16-
*
16+
*
1717
* <165>1 2003-10-11T22:14:15.003Z mymachine.example.com
1818
evntslog - ID47 [exampleSDID@0 iut="3" eventSource=
1919
"Application" eventID="1011"] BOMAn application
@@ -23,21 +23,44 @@
2323
public class FullSender implements MessageSender {
2424
private Logger log = Logger.getLogger(FullSender.class.getName());
2525

26+
/**
27+
* ThreadLocal HashMap to avoid allocating new HashMaps on every message
28+
*/
29+
private static final ThreadLocal<Map<String, String>> SD_PARAMS_CACHE = new ThreadLocal<Map<String, String>>() {
30+
@Override
31+
protected Map<String, String> initialValue() {
32+
return new HashMap<String, String>();
33+
}
34+
};
35+
36+
/**
37+
* ThreadLocal HashMap for structured data to avoid allocating new HashMaps on every message
38+
*/
39+
private static final ThreadLocal<Map<String, Map<String, String>>> SD_CACHE = new ThreadLocal<Map<String, Map<String, String>>>() {
40+
@Override
41+
protected Map<String, Map<String, String>> initialValue() {
42+
return new HashMap<String, Map<String, String>>();
43+
}
44+
};
45+
2646
@Override
27-
public void send(SyslogIF syslog, int level, Message msg) {
28-
Map<String, String> sdParams = new HashMap<String, String>();
47+
public void send(SyslogIF syslog, int level, Message msg) {
48+
Map<String, String> sdParams = SD_PARAMS_CACHE.get();
49+
sdParams.clear();
50+
2951
Map<String, Object> fields = msg.getFields();
3052
for (String key: fields.keySet()) {
3153
if (key != Message.FIELD_MESSAGE && key != Message.FIELD_FULL_MESSAGE && key != Message.FIELD_SOURCE) {
3254
sdParams.put(key, fields.get(key).toString());
3355
}
3456
}
35-
57+
3658
// http://www.iana.org/assignments/enterprise-numbers/enterprise-numbers
3759
// <name>@<enterpriseId>
3860
String sdId = "all@0";
3961
// log.info("Sending " + level + ", " + msg.getId() + ", " + msg.getSource() + ", " + sdId + "=" + sdParams + ", " + msg.getMessage());
40-
Map<String,Map<String,String>> sd = new HashMap<String, Map<String,String>>();
62+
Map<String,Map<String,String>> sd = SD_CACHE.get();
63+
sd.clear();
4164
sd.put(sdId, sdParams);
4265

4366
String msgId = null;
@@ -65,8 +88,19 @@ public void send(SyslogIF syslog, int level, Message msg) {
6588
syslog.log(level, new StructuredSyslogMessage(msgId, sourceId, sd, dumpMessage(msg)));
6689
}
6790

91+
/**
92+
* ThreadLocal StringBuilder for dumpMessage to avoid allocations
93+
*/
94+
private static final ThreadLocal<StringBuilder> DUMP_MESSAGE_BUILDER = new ThreadLocal<StringBuilder>() {
95+
@Override
96+
protected StringBuilder initialValue() {
97+
return new StringBuilder(512);
98+
}
99+
};
100+
68101
public static String dumpMessage(Message msg) {
69-
final StringBuilder sb = new StringBuilder();
102+
final StringBuilder sb = DUMP_MESSAGE_BUILDER.get();
103+
sb.setLength(0);
70104
sb.append("source: ").append(msg.getField(Message.FIELD_SOURCE)).append(" | ");
71105

72106
Object text = msg.getField(Message.FIELD_FULL_MESSAGE);

src/main/java/com/wizecore/graylog2/plugin/PlainSender.java

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,46 +11,68 @@
1111
import org.graylog2.syslog4j.SyslogIF;
1212

1313
/**
14-
* Formats fields into message text
15-
*
14+
* Formats fields into message text
15+
*
1616
1717
<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - BOM'su root' failed for lonvick on /dev/pts/8
1818
^priority
1919
^ version
20-
^ date
20+
^ date
2121
^ host
2222
^ APP-NAME
2323
^ structured data?
24-
^ MSGID
25-
24+
^ MSGID
25+
2626
*/
2727
public class PlainSender implements MessageSender {
2828
private Logger log = Logger.getLogger(PlainSender.class.getName());
2929

3030
public static final String SYSLOG_DATEFORMAT = "MMM dd HH:mm:ss";
31-
31+
32+
/**
33+
* ThreadLocal SimpleDateFormat to avoid creating new instances on every message
34+
* and avoid synchronization issues (SimpleDateFormat is not thread-safe)
35+
*/
36+
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
37+
@Override
38+
protected SimpleDateFormat initialValue() {
39+
return new SimpleDateFormat(SYSLOG_DATEFORMAT, Locale.ENGLISH);
40+
}
41+
};
42+
43+
/**
44+
* ThreadLocal StringBuilder to avoid allocating new StringBuilder on every message
45+
*/
46+
private static final ThreadLocal<StringBuilder> STRING_BUILDER_CACHE = new ThreadLocal<StringBuilder>() {
47+
@Override
48+
protected StringBuilder initialValue() {
49+
return new StringBuilder(256);
50+
}
51+
};
52+
3253
/**
3354
* From syslog4j
34-
*
55+
*
3556
* @param dt
3657
* @return
3758
*/
3859
public static void appendSyslogTimestamp(Date dt, StringBuilder buffer) {
39-
SimpleDateFormat dateFormat = new SimpleDateFormat(SYSLOG_DATEFORMAT,Locale.ENGLISH);
40-
String datePrefix = dateFormat.format(dt);
41-
42-
int pos = buffer.length() + 4;
60+
SimpleDateFormat dateFormat = DATE_FORMAT.get();
61+
String datePrefix = dateFormat.format(dt);
62+
63+
int pos = buffer.length() + 4;
4364
buffer.append(datePrefix);
44-
65+
4566
// RFC 3164 requires leading space for days 1-9
4667
if (buffer.charAt(pos) == '0') {
4768
buffer.setCharAt(pos,' ');
4869
}
4970
}
50-
71+
5172
@Override
5273
public void send(SyslogIF syslog, int level, Message msg) {
53-
StringBuilder out = new StringBuilder();
74+
StringBuilder out = STRING_BUILDER_CACHE.get();
75+
out.setLength(0);
5476
appendHeader(msg, out);
5577
out.append(msg.getMessage());
5678
String str = out.toString();

src/main/java/com/wizecore/graylog2/plugin/SnareWindowsSender.java

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,64 @@ public class SnareWindowsSender implements MessageSender {
2828
public static final String SYSLOG_DATEFORMAT = "MMM dd HH:mm:ss";
2929
public static final String MSEVENT_DATEFORMAT = "EEE MMM dd HH:mm:ss yyyy";
3030
public static final String SEPARATOR = "\t";
31+
32+
/**
33+
* ThreadLocal SimpleDateFormat to avoid creating new instances on every message
34+
* and avoid synchronization issues (SimpleDateFormat is not thread-safe)
35+
*/
36+
private static final ThreadLocal<SimpleDateFormat> SYSLOG_DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
37+
@Override
38+
protected SimpleDateFormat initialValue() {
39+
return new SimpleDateFormat(SYSLOG_DATEFORMAT, Locale.ENGLISH);
40+
}
41+
};
42+
43+
/**
44+
* ThreadLocal SimpleDateFormat for MS Event timestamp
45+
*/
46+
private static final ThreadLocal<SimpleDateFormat> MSEVENT_DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
47+
@Override
48+
protected SimpleDateFormat initialValue() {
49+
return new SimpleDateFormat(MSEVENT_DATEFORMAT, Locale.ENGLISH);
50+
}
51+
};
52+
53+
/**
54+
* ThreadLocal StringBuilder to avoid allocating new StringBuilder on every message
55+
*/
56+
private static final ThreadLocal<StringBuilder> STRING_BUILDER_CACHE = new ThreadLocal<StringBuilder>() {
57+
@Override
58+
protected StringBuilder initialValue() {
59+
return new StringBuilder(512);
60+
}
61+
};
62+
3163
/**
3264
* From syslog4j
33-
*
65+
*
3466
* @param dt
3567
* @return
3668
*/
3769
public static void appendSyslogTimestamp(Date dt, StringBuilder buffer) {
38-
SimpleDateFormat dateFormat = new SimpleDateFormat(SYSLOG_DATEFORMAT,Locale.ENGLISH);
39-
String datePrefix = dateFormat.format(dt);
40-
41-
int pos = buffer.length() + 4;
70+
SimpleDateFormat dateFormat = SYSLOG_DATE_FORMAT.get();
71+
String datePrefix = dateFormat.format(dt);
72+
73+
int pos = buffer.length() + 4;
4274
buffer.append(datePrefix);
43-
75+
4476
// RFC 3164 requires leading space for days 1-9
4577
if (buffer.charAt(pos) == '0') {
4678
buffer.setCharAt(pos,' ');
4779
}
4880
}
4981

5082
public static void appendMSEventTimestamp(Date dt, StringBuilder buffer) {
51-
SimpleDateFormat dateFormat = new SimpleDateFormat(MSEVENT_DATEFORMAT,Locale.ENGLISH);
52-
String datePrefix = dateFormat.format(dt);
53-
54-
int pos = buffer.length() + 4;
83+
SimpleDateFormat dateFormat = MSEVENT_DATE_FORMAT.get();
84+
String datePrefix = dateFormat.format(dt);
85+
86+
int pos = buffer.length() + 4;
5587
buffer.append(datePrefix);
56-
88+
5789
// RFC 3164 requires leading space for days 1-9
5890
if (buffer.charAt(pos) == '0') {
5991
buffer.setCharAt(pos,' ');
@@ -62,7 +94,8 @@ public static void appendMSEventTimestamp(Date dt, StringBuilder buffer) {
6294

6395
@Override
6496
public void send(SyslogIF syslog, int level, Message msg) {
65-
StringBuilder out = new StringBuilder();
97+
StringBuilder out = STRING_BUILDER_CACHE.get();
98+
out.setLength(0);
6699
//appendHeader(msg, out);
67100

68101

src/main/java/com/wizecore/graylog2/plugin/StructuredSender.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
/**
1212
* https://tools.ietf.org/html/rfc5424
13-
*
13+
*
1414
* <165>1 2003-10-11T22:14:15.003Z mymachine.example.com
1515
evntslog - ID47 [exampleSDID@0 iut="3" eventSource=
1616
"Application" eventID="1011"] BOMAn application
@@ -20,21 +20,44 @@
2020
public class StructuredSender implements MessageSender {
2121
private Logger log = Logger.getLogger(StructuredSender.class.getName());
2222

23+
/**
24+
* ThreadLocal HashMap to avoid allocating new HashMaps on every message
25+
*/
26+
private static final ThreadLocal<Map<String, String>> SD_PARAMS_CACHE = new ThreadLocal<Map<String, String>>() {
27+
@Override
28+
protected Map<String, String> initialValue() {
29+
return new HashMap<String, String>();
30+
}
31+
};
32+
33+
/**
34+
* ThreadLocal HashMap for structured data to avoid allocating new HashMaps on every message
35+
*/
36+
private static final ThreadLocal<Map<String, Map<String, String>>> SD_CACHE = new ThreadLocal<Map<String, Map<String, String>>>() {
37+
@Override
38+
protected Map<String, Map<String, String>> initialValue() {
39+
return new HashMap<String, Map<String, String>>();
40+
}
41+
};
42+
2343
@Override
24-
public void send(SyslogIF syslog, int level, Message msg) {
25-
Map<String, String> sdParams = new HashMap<String, String>();
44+
public void send(SyslogIF syslog, int level, Message msg) {
45+
Map<String, String> sdParams = SD_PARAMS_CACHE.get();
46+
sdParams.clear();
47+
2648
Map<String, Object> fields = msg.getFields();
2749
for (String key: fields.keySet()) {
2850
if (key != Message.FIELD_MESSAGE && key != Message.FIELD_FULL_MESSAGE && key != Message.FIELD_SOURCE) {
2951
sdParams.put(key, fields.get(key).toString());
3052
}
3153
}
32-
54+
3355
// http://www.iana.org/assignments/enterprise-numbers/enterprise-numbers
3456
// <name>@<enterpriseId>
3557
String sdId = "all@0";
3658
// log.info("Sending " + level + ", " + msg.getId() + ", " + msg.getSource() + ", " + sdId + "=" + sdParams + ", " + msg.getMessage());
37-
Map<String,Map<String,String>> sd = new HashMap<String, Map<String,String>>();
59+
Map<String,Map<String,String>> sd = SD_CACHE.get();
60+
sd.clear();
3861
sd.put(sdId, sdParams);
3962

4063
String msgId = null;

src/main/java/com/wizecore/graylog2/plugin/TransparentSyslogSender.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ public class TransparentSyslogSender implements MessageSender {
2323

2424
public static final String SYSLOG_DATEFORMAT = "MMM dd HH:mm:ss";
2525

26+
/**
27+
* ThreadLocal SimpleDateFormat to avoid creating new instances on every message
28+
* and avoid synchronization issues (SimpleDateFormat is not thread-safe)
29+
*/
30+
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
31+
@Override
32+
protected SimpleDateFormat initialValue() {
33+
return new SimpleDateFormat(SYSLOG_DATEFORMAT, Locale.ENGLISH);
34+
}
35+
};
36+
37+
/**
38+
* ThreadLocal StringBuilder to avoid allocating new StringBuilder on every message
39+
*/
40+
private static final ThreadLocal<StringBuilder> STRING_BUILDER_CACHE = new ThreadLocal<StringBuilder>() {
41+
@Override
42+
protected StringBuilder initialValue() {
43+
return new StringBuilder(256);
44+
}
45+
};
46+
2647
public TransparentSyslogSender(Configuration conf) {
2748
removeHeader = conf.getBoolean("transparentFormatRemoveHeader");
2849
}
@@ -35,7 +56,7 @@ public TransparentSyslogSender(Configuration conf) {
3556
* @return
3657
*/
3758
public static void appendSyslogTimestamp(Message msg, StringBuilder buffer) {
38-
SimpleDateFormat dateFormat = new SimpleDateFormat(SYSLOG_DATEFORMAT, Locale.ENGLISH);
59+
SimpleDateFormat dateFormat = DATE_FORMAT.get();
3960

4061
Date dt = null;
4162
Object ts = msg.getField("timestamp");
@@ -170,7 +191,8 @@ protected void appendPriority(Message msg, int level, StringBuilder out) {
170191

171192
@Override
172193
public void send(SyslogIF syslog, int level, Message msg) {
173-
StringBuilder out = new StringBuilder();
194+
StringBuilder out = STRING_BUILDER_CACHE.get();
195+
out.setLength(0);
174196
if (!removeHeader) {
175197
appendHeader(msg, level, out);
176198
}

0 commit comments

Comments
 (0)