1 package org
.argeo
.init
.logging
;
3 import java
.io
.PrintStream
;
4 import java
.io
.Serializable
;
5 import java
.lang
.System
.Logger
;
6 import java
.lang
.System
.Logger
.Level
;
7 import java
.text
.MessageFormat
;
8 import java
.time
.Instant
;
9 import java
.util
.Collections
;
10 import java
.util
.HashMap
;
11 import java
.util
.Iterator
;
13 import java
.util
.NavigableMap
;
14 import java
.util
.Objects
;
15 import java
.util
.ResourceBundle
;
16 import java
.util
.SortedMap
;
17 import java
.util
.StringTokenizer
;
18 import java
.util
.TreeMap
;
19 import java
.util
.concurrent
.Executor
;
20 import java
.util
.concurrent
.ExecutorService
;
21 import java
.util
.concurrent
.Executors
;
22 import java
.util
.concurrent
.Flow
;
23 import java
.util
.concurrent
.Flow
.Subscription
;
24 import java
.util
.concurrent
.SubmissionPublisher
;
25 import java
.util
.concurrent
.TimeUnit
;
26 import java
.util
.concurrent
.atomic
.AtomicLong
;
27 import java
.util
.function
.Consumer
;
30 * A thin logging system based on the {@link Logger} framework. It is a
31 * {@link Consumer} of configuration, and can be registered as such.
33 class ThinLogging
implements Consumer
<Map
<String
, Object
>> {
34 final static String DEFAULT_LEVEL_NAME
= "";
36 final static String DEFAULT_LEVEL_PROPERTY
= "log";
37 final static String LEVEL_PROPERTY_PREFIX
= DEFAULT_LEVEL_PROPERTY
+ ".";
39 final static String JOURNALD_PROPERTY
= "argeo.logging.journald";
40 final static String CALL_LOCATION_PROPERTY
= "argeo.logging.callLocation";
42 private final static AtomicLong nextEntry
= new AtomicLong(0l);
44 // we don't synchronize maps on purpose as it would be
45 // too expensive during normal operation
46 // updates to the config may be shortly inconsistent
47 private SortedMap
<String
, ThinLogger
> loggers
= new TreeMap
<>();
48 private NavigableMap
<String
, Level
> levels
= new TreeMap
<>();
49 private volatile boolean updatingConfiguration
= false;
51 private final ExecutorService executor
;
52 private final LogEntryPublisher publisher
;
54 private final boolean journald
;
55 private final Level callLocationLevel
;
58 executor
= Executors
.newCachedThreadPool((r
) -> {
59 Thread t
= new Thread(r
);
63 publisher
= new LogEntryPublisher(executor
, Flow
.defaultBufferSize());
65 PrintStreamSubscriber subscriber
= new PrintStreamSubscriber();
66 publisher
.subscribe(subscriber
);
68 Runtime
.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown"));
70 // initial default level
71 levels
.put("", Level
.WARNING
);
73 // Logging system config
76 // Map<String, String> env = new TreeMap<>(System.getenv());
77 // for (String key : env.keySet()) {
78 // System.out.println(key + "=" + env.get(key));
81 String journaldStr
= System
.getProperty(JOURNALD_PROPERTY
, "auto");
82 switch (journaldStr
) {
84 String systemdInvocationId
= System
.getenv("INVOCATION_ID");
85 if (systemdInvocationId
!= null) {// in systemd
86 // check whether we are indirectly in a desktop app (e.g. eclipse)
87 String desktopFilePid
= System
.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID");
88 if (desktopFilePid
!= null) {
89 Long javaPid
= ProcessHandle
.current().pid();
90 if (!javaPid
.toString().equals(desktopFilePid
)) {
109 throw new IllegalArgumentException(
110 "Unsupported value '" + journaldStr
+ "' for property " + JOURNALD_PROPERTY
);
113 String callLocationStr
= System
.getProperty(CALL_LOCATION_PROPERTY
, Level
.WARNING
.getName());
114 callLocationLevel
= Level
.valueOf(callLocationStr
);
117 private void close() {
120 // we ait a bit in order to make sure all messages are flushed
121 // TODO synchronize more efficiently
122 executor
.awaitTermination(300, TimeUnit
.MILLISECONDS
);
123 } catch (InterruptedException e
) {
128 private Level
computeApplicableLevel(String name
) {
129 Map
.Entry
<String
, Level
> entry
= levels
.floorEntry(name
);
130 assert entry
!= null;
131 return entry
.getValue();
135 // private boolean isLoggable(String name, Level level) {
136 // Objects.requireNonNull(name);
137 // Objects.requireNonNull(level);
139 // if (updatingConfiguration) {
140 // synchronized (levels) {
143 // // TODO make exit more robust
144 // } catch (InterruptedException e) {
145 // throw new IllegalStateException(e);
150 // return level.getSeverity() >= computeApplicableLevel(name).getSeverity();
153 public Logger
getLogger(String name
, Module module
) {
154 if (!loggers
.containsKey(name
)) {
155 ThinLogger logger
= new ThinLogger(name
, computeApplicableLevel(name
));
156 loggers
.put(name
, logger
);
158 return loggers
.get(name
);
161 public void accept(Map
<String
, Object
> configuration
) {
162 synchronized (levels
) {
163 updatingConfiguration
= true;
165 Map
<String
, Level
> backup
= new TreeMap
<>(levels
);
167 boolean fullReset
= configuration
.containsKey(DEFAULT_LEVEL_PROPERTY
);
169 properties
: for (String property
: configuration
.keySet()) {
170 if (!property
.startsWith(LEVEL_PROPERTY_PREFIX
))
172 String levelStr
= configuration
.get(property
).toString();
173 Level level
= Level
.valueOf(levelStr
);
174 levels
.put(property
.substring(LEVEL_PROPERTY_PREFIX
.length()), level
);
178 Iterator
<Map
.Entry
<String
, Level
>> it
= levels
.entrySet().iterator();
179 while (it
.hasNext()) {
180 Map
.Entry
<String
, Level
> entry
= it
.next();
181 String name
= entry
.getKey();
182 if (!configuration
.containsKey(LEVEL_PROPERTY_PREFIX
+ name
)) {
186 Level newDefaultLevel
= Level
.valueOf(configuration
.get(DEFAULT_LEVEL_PROPERTY
).toString());
187 levels
.put(DEFAULT_LEVEL_NAME
, newDefaultLevel
);
188 // TODO notify everyone?
190 assert levels
.containsKey(DEFAULT_LEVEL_NAME
);
192 // recompute all levels
193 for (String name
: loggers
.keySet()) {
194 ThinLogger logger
= loggers
.get(name
);
195 logger
.setLevel(computeApplicableLevel(name
));
197 } catch (IllegalArgumentException e
) {
200 levels
.putAll(backup
);
202 updatingConfiguration
= false;
208 Flow
.Publisher
<Map
<String
, Serializable
>> getLogEntryPublisher() {
212 Map
<String
, Level
> getLevels() {
213 return Collections
.unmodifiableNavigableMap(levels
);
220 private class ThinLogger
implements System
.Logger
{
221 private final String name
;
225 protected ThinLogger(String name
, Level level
) {
226 assert Objects
.nonNull(name
);
232 public String
getName() {
237 public boolean isLoggable(Level level
) {
238 return level
.getSeverity() >= getLevel().getSeverity();
239 // TODO optimise by referencing the applicable level in this class?
240 // return ThinLogging.this.isLoggable(name, level);
243 private Level
getLevel() {
244 if (updatingConfiguration
) {
245 synchronized (levels
) {
248 // TODO make exit more robust
249 } catch (InterruptedException e
) {
250 throw new IllegalStateException(e
);
258 public void log(Level level
, ResourceBundle bundle
, String msg
, Throwable thrown
) {
259 if (!isLoggable(level
))
261 // measure timestamp first
262 Instant now
= Instant
.now();
263 Thread thread
= Thread
.currentThread();
264 publisher
.log(this, level
, bundle
, msg
, now
, thread
, thrown
, findCallLocation(level
, thread
));
268 public void log(Level level
, ResourceBundle bundle
, String format
, Object
... params
) {
269 if (!isLoggable(level
))
271 // measure timestamp first
272 Instant now
= Instant
.now();
273 Thread thread
= Thread
.currentThread();
275 // NOTE: this is the method called when logging a plain message without
276 // exception, so it should be considered as a format only when args are not null
277 String msg
= params
== null ? format
: MessageFormat
.format(format
, params
);
278 publisher
.log(this, level
, bundle
, msg
, now
, thread
, (Throwable
) null, findCallLocation(level
, thread
));
281 private void setLevel(Level level
) {
285 private StackTraceElement
findCallLocation(Level level
, Thread thread
) {
286 assert level
!= null;
287 assert thread
!= null;
288 // TODO rather use a StackWalker and make it smarter
289 StackTraceElement callLocation
= null;
290 if (level
.getSeverity() >= callLocationLevel
.getSeverity()) {
291 StackTraceElement
[] stack
= thread
.getStackTrace();
292 int lowestLoggerInterface
= 0;
293 stack
: for (int i
= 2; i
< stack
.length
; i
++) {
294 String className
= stack
[i
].getClassName();
296 // TODO make it more configurable
297 // FIXME deal with privileges stacks (in Equinox)
298 case "java.lang.System$Logger":
299 case "java.util.logging.Logger":
300 case "org.apache.commons.logging.Log":
301 case "org.osgi.service.log.Logger":
302 case "org.eclipse.osgi.internal.log.LoggerImpl":
303 case "org.argeo.api.cms.CmsLog":
304 case "org.slf4j.impl.ArgeoLogger":
305 case "org.argeo.cms.internal.osgi.CmsOsgiLogger":
306 case "org.eclipse.jetty.util.log.Slf4jLog":
307 case "sun.util.logging.internal.LoggingProviderImpl$JULWrapper":
308 lowestLoggerInterface
= i
;
313 if (stack
.length
> lowestLoggerInterface
+ 1)
314 callLocation
= stack
[lowestLoggerInterface
+ 1];
321 private final static String KEY_LOGGER
= Logger
.class.getName();
322 private final static String KEY_LEVEL
= Level
.class.getName();
323 private final static String KEY_MSG
= String
.class.getName();
324 private final static String KEY_THROWABLE
= Throwable
.class.getName();
325 private final static String KEY_INSTANT
= Instant
.class.getName();
326 private final static String KEY_CALL_LOCATION
= StackTraceElement
.class.getName();
327 private final static String KEY_THREAD
= Thread
.class.getName();
329 private class LogEntryPublisher
extends SubmissionPublisher
<Map
<String
, Serializable
>> {
331 private LogEntryPublisher(Executor executor
, int maxBufferCapacity
) {
332 super(executor
, maxBufferCapacity
);
335 private void log(ThinLogger logger
, Level level
, ResourceBundle bundle
, String msg
, Instant instant
,
336 Thread thread
, Throwable thrown
, StackTraceElement callLocation
) {
337 assert level
!= null;
338 assert logger
!= null;
340 assert instant
!= null;
341 assert thread
!= null;
343 final long sequence
= nextEntry
.incrementAndGet();
345 Map
<String
, Serializable
> logEntry
= new LogEntryMap(sequence
);
347 // same object as key class name
348 logEntry
.put(KEY_LEVEL
, level
);
349 logEntry
.put(KEY_MSG
, msg
);
350 logEntry
.put(KEY_INSTANT
, instant
);
352 logEntry
.put(KEY_THROWABLE
, thrown
);
353 if (callLocation
!= null)
354 logEntry
.put(KEY_CALL_LOCATION
, callLocation
);
356 // object is a string
357 logEntry
.put(KEY_LOGGER
, logger
.getName());
358 logEntry
.put(KEY_THREAD
, thread
.getName());
360 // should be unmodifiable for security reasons
361 submit(Collections
.unmodifiableMap(logEntry
));
367 * An internal optimisation for collections. It should not be referred to
368 * directly as a type.
370 // TODO optimise memory with a custom map implementation?
371 // but access may be slower
372 private static class LogEntryMap
extends HashMap
<String
, Serializable
> {
373 private static final long serialVersionUID
= 7361434381922521356L;
375 private final long sequence
;
377 private LogEntryMap(long sequence
) {
378 // maximum 7 fields, so using default load factor 0.75
379 // an initial size of 10 should prevent rehashing (7 / 0.75 ~ 9.333)
380 // see HashMap class description for more details
382 this.sequence
= sequence
;
386 public boolean equals(Object o
) {
387 if (o
instanceof LogEntryMap
)
388 return sequence
== ((LogEntryMap
) o
).sequence
;
389 else if (o
instanceof Map
) {
390 Map
<?
, ?
> map
= (Map
<?
, ?
>) o
;
391 return get(KEY_INSTANT
).equals(map
.get(KEY_INSTANT
)) && get(KEY_THREAD
).equals(map
.get(KEY_THREAD
));
397 public int hashCode() {
398 return (int) sequence
;
403 private class PrintStreamSubscriber
implements Flow
.Subscriber
<Map
<String
, Serializable
>> {
404 private PrintStream out
;
405 private PrintStream err
;
406 private int writeToErrLevel
= Level
.WARNING
.getSeverity();
408 protected PrintStreamSubscriber() {
409 this(System
.out
, System
.err
);
412 protected PrintStreamSubscriber(PrintStream out
, PrintStream err
) {
417 private Level
getLevel(Map
<String
, Serializable
> logEntry
) {
418 return (Level
) logEntry
.get(KEY_LEVEL
);
422 public void onSubscribe(Subscription subscription
) {
423 subscription
.request(Long
.MAX_VALUE
);
427 public void onNext(Map
<String
, Serializable
> item
) {
428 if (getLevel(item
).getSeverity() >= writeToErrLevel
) {
429 err
.print(toPrint(item
));
431 out
.print(toPrint(item
));
433 // TODO flush for journald?
437 public void onError(Throwable throwable
) {
438 throwable
.printStackTrace(err
);
442 public void onComplete() {
447 protected String
firstLinePrefix(Map
<String
, Serializable
> logEntry
) {
448 Level level
= getLevel(logEntry
);
466 throw new IllegalArgumentException("Unsupported level " + level
);
468 return journald ?
linePrefix(logEntry
) : logEntry
.get(KEY_INSTANT
) + " " + level
+ spaces
;
471 protected String
firstLineSuffix(Map
<String
, Serializable
> logEntry
) {
472 return " - " + (logEntry
.containsKey(KEY_CALL_LOCATION
) ? logEntry
.get(KEY_CALL_LOCATION
)
473 : logEntry
.get(KEY_LOGGER
)) + " [" + logEntry
.get(KEY_THREAD
) + "]";
476 protected String
linePrefix(Map
<String
, Serializable
> logEntry
) {
477 return journald ?
"<" + levelToJournald(getLevel(logEntry
)) + ">" : "";
480 protected int levelToJournald(Level level
) {
481 int severity
= level
.getSeverity();
482 if (severity
>= Level
.ERROR
.getSeverity())
484 else if (severity
>= Level
.WARNING
.getSeverity())
486 else if (severity
>= Level
.INFO
.getSeverity())
492 protected String
toPrint(Map
<String
, Serializable
> logEntry
) {
493 StringBuilder sb
= new StringBuilder();
494 StringTokenizer st
= new StringTokenizer((String
) logEntry
.get(KEY_MSG
), "\r\n");
495 assert st
.hasMoreTokens();
498 String firstLine
= st
.nextToken();
499 sb
.append(firstLinePrefix(logEntry
));
500 sb
.append(firstLine
);
501 sb
.append(firstLineSuffix(logEntry
));
505 String prefix
= linePrefix(logEntry
);
506 while (st
.hasMoreTokens()) {
508 sb
.append(st
.nextToken());
512 if (logEntry
.containsKey(KEY_THROWABLE
)) {
513 Throwable throwable
= (Throwable
) logEntry
.get(KEY_THROWABLE
);
515 addThrowable(sb
, prefix
, throwable
);
517 return sb
.toString();
520 protected void addThrowable(StringBuilder sb
, String prefix
, Throwable throwable
) {
521 sb
.append(throwable
.getClass().getName());
523 sb
.append(throwable
.getMessage());
525 for (StackTraceElement ste
: throwable
.getStackTrace()) {
528 sb
.append(ste
.toString());
531 if (throwable
.getCause() != null) {
533 sb
.append("Caused by: ");
534 addThrowable(sb
, prefix
, throwable
.getCause());
539 public static void main(String args
[]) {
540 Logger logger
= System
.getLogger(ThinLogging
.class.getName());
541 logger
.log(Logger
.Level
.ALL
, "Log all");
542 logger
.log(Logger
.Level
.TRACE
, "Multi\nline\ntrace");
543 logger
.log(Logger
.Level
.DEBUG
, "Log debug");
544 logger
.log(Logger
.Level
.INFO
, "Log info");
545 logger
.log(Logger
.Level
.WARNING
, "Log warning");
546 logger
.log(Logger
.Level
.ERROR
, "Log exception", new Throwable());