-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
CappedLogger.java
312 lines (279 loc) · 10.4 KB
/
CappedLogger.java
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
* Copyright (c) 2002-2015 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.impl.util;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import org.neo4j.helpers.Clock;
/**
* A CappedLogger will accept log messages, unless they occur "too much", in which case the messages will be ignored
* until some time passes, or the logger is reset.
*
* This class does not extend {@link StringLogger} because that API is a mess, and has been replaced in newer versions
* of Neo4j.
*
* It is also desirable to be aware that log capping is taking place, so we don't mistakenly lose log output due to
* output capping.
*
* By default, the CappedLogger does not filter out any messages. Filtering can be configured at any time using the
* "set" and "unset" methods.
*/
public class CappedLogger
{
private final StringLogger delegate;
// We use the filter indirection so we can atomically update the configuration without locking
private volatile Filter filter;
public CappedLogger( StringLogger delegate )
{
if ( delegate == null )
{
throw new IllegalArgumentException( "The delegate StringLogger cannot be null" );
}
filter = new Filter();
this.delegate = delegate;
}
public void debug( String msg, Throwable cause )
{
if ( filter.accept( msg, cause ) )
{
delegate.debug( msg, cause );
}
}
public void info( String msg, Throwable cause )
{
if ( filter.accept( msg, cause ) )
{
delegate.info( msg, cause );
}
}
public void warn( String msg, Throwable cause )
{
if ( filter.accept( msg, cause ) )
{
delegate.warn( msg, cause );
}
}
public void error( String msg, Throwable cause )
{
if ( filter.accept( msg, cause ) )
{
delegate.error( msg, cause );
}
}
/**
* Reset the filtering state of this CappedLogger. This usually means that something good happened, and that all
* filtering states that grows towards a state where the log messages are filtered out, should calm down and reset.
*
* Specifically, this means that the counter in the count limit should return to zero, and that the time limit and
* duplicate filter should forget about past messages.
*/
public void reset()
{
filter = filter.reset();
}
/**
* Set a limit to the amount of logging that this logger will accept between resets.
* @param limit The number of log messages that the CappedLogger will let through in between resets.
*/
public CappedLogger setCountLimit( int limit )
{
if ( limit < 1 )
{
throw new IllegalArgumentException( "The count limit must be positive" );
}
filter = filter.setCountLimit( limit );
return this;
}
/**
* Unset the count limit, and allow any number of messages through, provided other limits don't apply.
*/
public CappedLogger unsetCountLimit()
{
filter = filter.unsetCountLimit();
return this;
}
/**
* Set a time based limit to the amount of logging that this logger will accept between resets. With a time limit
* of 1 second, for instance, then the logger will log at most one message per second.
* @param time The time amount, must be positive.
* @param unit The time unit.
* @param clock The clock to use for reading the current time when checking this limit.
*/
public CappedLogger setTimeLimit( long time, TimeUnit unit, Clock clock )
{
if ( time < 1 )
{
throw new IllegalArgumentException( "The time limit must be positive" );
}
if ( unit == null )
{
throw new IllegalArgumentException( "The time unit cannot be null" );
}
if ( clock == null )
{
throw new IllegalArgumentException( "The clock used for time limiting cannot be null" );
}
filter = filter.setTimeLimit( time, unit, clock );
return this;
}
/**
* Unset the time limit, and allow any number of messages through, as often as possible, provided other limits
* don't apply.
*/
public CappedLogger unsetTimeLimit()
{
filter = filter.unsetTimeLimit();
return this;
}
/**
* Enable or disable filtering of duplicate messages. This filtering only looks at the previous message, so a
* sequence of identical messages will only have that message logged once, but a sequence of two alternating
* messages will get logged in full.
* @param enabled {@code true} if duplicates should be filtered, {@code false} if they should not.
*/
public CappedLogger setDuplicateFilterEnabled( boolean enabled )
{
filter = filter.setDuplicateFilterEnabled( enabled );
return this;
}
private static class Filter
{
private static final AtomicIntegerFieldUpdater<Filter> CURRENT_COUNT =
AtomicIntegerFieldUpdater.newUpdater( Filter.class, "currentCount" );
private static final AtomicLongFieldUpdater<Filter> LAST_CHECK =
AtomicLongFieldUpdater.newUpdater( Filter.class, "lastCheck" );
// The thread-safety of these normal fields are guarded by the volatile reads and writes to the
// CappedLogger.filter field.
private boolean hasCountLimit;
private int countLimit;
private long timeLimitMillis;
private Clock clock;
private boolean filterDuplicates;
// Atomically updated
private volatile int currentCount;
private volatile long lastCheck;
// Read and updated together; guarded by synchronized(this) in checkDuplicate()
private String lastMessage;
private Throwable lastException;
private Filter()
{
this( false, 0, 0, 0, 0, null, false );
}
private Filter(
boolean hasCountLimit,
int countLimit,
int currentCount,
long timeLimitMillis,
long lastCheck,
Clock clock, boolean filterDuplicates )
{
this.hasCountLimit = hasCountLimit;
this.countLimit = countLimit;
this.currentCount = currentCount;
this.timeLimitMillis = timeLimitMillis;
this.lastCheck = lastCheck;
this.clock = clock;
this.filterDuplicates = filterDuplicates;
}
public Filter setCountLimit( int limit )
{
return new Filter( true, limit, currentCount, timeLimitMillis, lastCheck, clock, filterDuplicates );
}
public boolean accept( String msg, Throwable cause )
{
return (!hasCountLimit || (getAndIncrementCurrentCount() < countLimit))
&& (clock == null || !checkExpiredAndSetLastCheckTime())
&& (!filterDuplicates || checkDuplicate( msg, cause ));
}
public int getAndIncrementCurrentCount()
{
return CURRENT_COUNT.getAndIncrement( this );
}
private boolean checkExpiredAndSetLastCheckTime()
{
long now = clock.currentTimeMillis();
long check = this.lastCheck;
if ( check > now - timeLimitMillis )
{
return true;
}
while ( !LAST_CHECK.compareAndSet( this, check, now ) )
{
check = lastCheck;
if ( check > now )
{
break;
}
}
return false;
}
private synchronized boolean checkDuplicate( String msg, Throwable cause )
{
String last = lastMessage;
Throwable exc = lastException;
if ( stringEqual( last, msg )
&& ( exc == null ? cause == null : sameClass( cause, exc ) && sameMsg( cause, exc ) ) )
{
// Duplicate! Filter it out.
return false;
}
else
{
// Distinct! Update and let it through.
lastMessage = msg;
lastException = cause;
return true;
}
}
private boolean sameMsg( Throwable cause, Throwable exc )
{
return stringEqual( exc.getMessage(), cause.getMessage() );
}
private boolean stringEqual( String a, String b )
{
return a == null ? b == null : a.equals( b );
}
private boolean sameClass( Throwable cause, Throwable exc )
{
return exc.getClass().equals( cause.getClass() );
}
public Filter reset()
{
return new Filter( hasCountLimit, countLimit, 0, timeLimitMillis, 0, clock, filterDuplicates );
}
public Filter unsetCountLimit()
{
return new Filter( false, 0, currentCount, timeLimitMillis, lastCheck, clock, filterDuplicates );
}
public Filter setTimeLimit( long time, TimeUnit unit, Clock clock )
{
return new Filter(
hasCountLimit, countLimit, currentCount, unit.toMillis( time ), lastCheck, clock, filterDuplicates );
}
public Filter unsetTimeLimit()
{
return new Filter( hasCountLimit, countLimit, currentCount, 0, lastCheck, null, filterDuplicates );
}
public Filter setDuplicateFilterEnabled( boolean enabled )
{
return new Filter( hasCountLimit, countLimit, currentCount, timeLimitMillis, lastCheck, clock, enabled );
}
}
}