]> git.argeo.org Git - lgpl/argeo-commons.git/blob - argeo/init/logging/ThinLogging.java
Prepare next development cycle
[lgpl/argeo-commons.git] / argeo / init / logging / ThinLogging.java
1 package org.argeo.init.logging;
2
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;
12 import java.util.Map;
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;
28
29 /**
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.
32 */
33 class ThinLogging implements Consumer<Map<String, Object>> {
34 final static String DEFAULT_LEVEL_NAME = "";
35
36 final static String DEFAULT_LEVEL_PROPERTY = "log";
37 final static String LEVEL_PROPERTY_PREFIX = DEFAULT_LEVEL_PROPERTY + ".";
38
39 final static String JOURNALD_PROPERTY = "argeo.logging.journald";
40 final static String CALL_LOCATION_PROPERTY = "argeo.logging.callLocation";
41
42 private final static AtomicLong nextEntry = new AtomicLong(0l);
43
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;
50
51 private final ExecutorService executor;
52 private final LogEntryPublisher publisher;
53
54 private final boolean journald;
55 private final Level callLocationLevel;
56
57 ThinLogging() {
58 executor = Executors.newCachedThreadPool((r) -> {
59 Thread t = new Thread(r);
60 t.setDaemon(true);
61 return t;
62 });
63 publisher = new LogEntryPublisher(executor, Flow.defaultBufferSize());
64
65 PrintStreamSubscriber subscriber = new PrintStreamSubscriber();
66 publisher.subscribe(subscriber);
67
68 Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown"));
69
70 // initial default level
71 levels.put("", Level.WARNING);
72
73 // Logging system config
74 // journald
75
76 // Map<String, String> env = new TreeMap<>(System.getenv());
77 // for (String key : env.keySet()) {
78 // System.out.println(key + "=" + env.get(key));
79 // }
80
81 String journaldStr = System.getProperty(JOURNALD_PROPERTY, "auto");
82 switch (journaldStr) {
83 case "auto":
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)) {
91 journald = false;
92 break;
93 }
94 }
95 journald = true;
96 break;
97 }
98 journald = false;
99 break;
100 case "true":
101 case "on":
102 journald = true;
103 break;
104 case "false":
105 case "off":
106 journald = false;
107 break;
108 default:
109 throw new IllegalArgumentException(
110 "Unsupported value '" + journaldStr + "' for property " + JOURNALD_PROPERTY);
111 }
112
113 String callLocationStr = System.getProperty(CALL_LOCATION_PROPERTY, Level.WARNING.getName());
114 callLocationLevel = Level.valueOf(callLocationStr);
115 }
116
117 private void close() {
118 publisher.close();
119 try {
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) {
124 // silent
125 }
126 }
127
128 private Level computeApplicableLevel(String name) {
129 Map.Entry<String, Level> entry = levels.floorEntry(name);
130 assert entry != null;
131 return entry.getValue();
132
133 }
134
135 // private boolean isLoggable(String name, Level level) {
136 // Objects.requireNonNull(name);
137 // Objects.requireNonNull(level);
138 //
139 // if (updatingConfiguration) {
140 // synchronized (levels) {
141 // try {
142 // levels.wait();
143 // // TODO make exit more robust
144 // } catch (InterruptedException e) {
145 // throw new IllegalStateException(e);
146 // }
147 // }
148 // }
149 //
150 // return level.getSeverity() >= computeApplicableLevel(name).getSeverity();
151 // }
152
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);
157 }
158 return loggers.get(name);
159 }
160
161 public void accept(Map<String, Object> configuration) {
162 synchronized (levels) {
163 updatingConfiguration = true;
164
165 Map<String, Level> backup = new TreeMap<>(levels);
166
167 boolean fullReset = configuration.containsKey(DEFAULT_LEVEL_PROPERTY);
168 try {
169 properties: for (String property : configuration.keySet()) {
170 if (!property.startsWith(LEVEL_PROPERTY_PREFIX))
171 continue properties;
172 String levelStr = configuration.get(property).toString();
173 Level level = Level.valueOf(levelStr);
174 levels.put(property.substring(LEVEL_PROPERTY_PREFIX.length()), level);
175 }
176
177 if (fullReset) {
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)) {
183 it.remove();
184 }
185 }
186 Level newDefaultLevel = Level.valueOf(configuration.get(DEFAULT_LEVEL_PROPERTY).toString());
187 levels.put(DEFAULT_LEVEL_NAME, newDefaultLevel);
188 // TODO notify everyone?
189 }
190 assert levels.containsKey(DEFAULT_LEVEL_NAME);
191
192 // recompute all levels
193 for (String name : loggers.keySet()) {
194 ThinLogger logger = loggers.get(name);
195 logger.setLevel(computeApplicableLevel(name));
196 }
197 } catch (IllegalArgumentException e) {
198 e.printStackTrace();
199 levels.clear();
200 levels.putAll(backup);
201 }
202 updatingConfiguration = false;
203 levels.notifyAll();
204 }
205
206 }
207
208 Flow.Publisher<Map<String, Serializable>> getLogEntryPublisher() {
209 return publisher;
210 }
211
212 Map<String, Level> getLevels() {
213 return Collections.unmodifiableNavigableMap(levels);
214 }
215
216 /*
217 * INTERNAL CLASSES
218 */
219
220 private class ThinLogger implements System.Logger {
221 private final String name;
222
223 private Level level;
224
225 protected ThinLogger(String name, Level level) {
226 assert Objects.nonNull(name);
227 this.name = name;
228 this.level = level;
229 }
230
231 @Override
232 public String getName() {
233 return name;
234 }
235
236 @Override
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);
241 }
242
243 private Level getLevel() {
244 if (updatingConfiguration) {
245 synchronized (levels) {
246 try {
247 levels.wait();
248 // TODO make exit more robust
249 } catch (InterruptedException e) {
250 throw new IllegalStateException(e);
251 }
252 }
253 }
254 return level;
255 }
256
257 @Override
258 public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
259 if (!isLoggable(level))
260 return;
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));
265 }
266
267 @Override
268 public void log(Level level, ResourceBundle bundle, String format, Object... params) {
269 if (!isLoggable(level))
270 return;
271 // measure timestamp first
272 Instant now = Instant.now();
273 Thread thread = Thread.currentThread();
274
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));
279 }
280
281 private void setLevel(Level level) {
282 this.level = level;
283 }
284
285 private StackTraceElement findCallLocation(Level level, Thread thread) {
286 assert level != null;
287 assert thread != null;
288 StackTraceElement callLocation = null;
289 if (level.getSeverity() >= callLocationLevel.getSeverity()) {
290 StackTraceElement[] stack = thread.getStackTrace();
291 int lowestLoggerInterface = 0;
292 stack: for (int i = 2; i < stack.length; i++) {
293 String className = stack[i].getClassName();
294 switch (className) {
295 // TODO make it more configurable
296 case "java.lang.System$Logger":
297 case "java.util.logging.Logger":
298 case "org.apache.commons.logging.Log":
299 case "org.osgi.service.log.Logger":
300 case "org.argeo.cms.Log":
301 case "org.slf4j.impl.ArgeoLogger":
302 case "org.eclipse.jetty.util.log.Slf4jLog":
303 lowestLoggerInterface = i;
304 continue stack;
305 default:
306 }
307 }
308 if (stack.length > lowestLoggerInterface + 1)
309 callLocation = stack[lowestLoggerInterface + 1];
310 }
311 return callLocation;
312 }
313
314 }
315
316 private final static String KEY_LOGGER = Logger.class.getName();
317 private final static String KEY_LEVEL = Level.class.getName();
318 private final static String KEY_MSG = String.class.getName();
319 private final static String KEY_THROWABLE = Throwable.class.getName();
320 private final static String KEY_INSTANT = Instant.class.getName();
321 private final static String KEY_CALL_LOCATION = StackTraceElement.class.getName();
322 private final static String KEY_THREAD = Thread.class.getName();
323
324 private class LogEntryPublisher extends SubmissionPublisher<Map<String, Serializable>> {
325
326 private LogEntryPublisher(Executor executor, int maxBufferCapacity) {
327 super(executor, maxBufferCapacity);
328 }
329
330 private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant,
331 Thread thread, Throwable thrown, StackTraceElement callLocation) {
332 assert level != null;
333 assert logger != null;
334 assert msg != null;
335 assert instant != null;
336 assert thread != null;
337
338 final long sequence = nextEntry.incrementAndGet();
339
340 Map<String, Serializable> logEntry = new LogEntryMap(sequence);
341
342 // same object as key class name
343 logEntry.put(KEY_LEVEL, level);
344 logEntry.put(KEY_MSG, msg);
345 logEntry.put(KEY_INSTANT, instant);
346 if (thrown != null)
347 logEntry.put(KEY_THROWABLE, thrown);
348 if (callLocation != null)
349 logEntry.put(KEY_CALL_LOCATION, callLocation);
350
351 // object is a string
352 logEntry.put(KEY_LOGGER, logger.getName());
353 logEntry.put(KEY_THREAD, thread.getName());
354
355 // should be unmodifiable for security reasons
356 submit(Collections.unmodifiableMap(logEntry));
357 }
358
359 }
360
361 /**
362 * An internal optimisation for collections. It should not be referred to
363 * directly as a type.
364 */
365 // TODO optimise memory with a custom map implementation?
366 // but access may be slower
367 private static class LogEntryMap extends HashMap<String, Serializable> {
368 private static final long serialVersionUID = 7361434381922521356L;
369
370 private final long sequence;
371
372 private LogEntryMap(long sequence) {
373 // maximum 7 fields, so using default load factor 0.75
374 // an initial size of 10 should prevent rehashing (7 / 0.75 ~ 9.333)
375 // see HashMap class description for more details
376 super(10, 0.75f);
377 this.sequence = sequence;
378 }
379
380 @Override
381 public boolean equals(Object o) {
382 if (o instanceof LogEntryMap)
383 return sequence == ((LogEntryMap) o).sequence;
384 else if (o instanceof Map) {
385 Map<?, ?> map = (Map<?, ?>) o;
386 return get(KEY_INSTANT).equals(map.get(KEY_INSTANT)) && get(KEY_THREAD).equals(map.get(KEY_THREAD));
387 } else
388 return false;
389 }
390
391 @Override
392 public int hashCode() {
393 return (int) sequence;
394 }
395
396 }
397
398 private class PrintStreamSubscriber implements Flow.Subscriber<Map<String, Serializable>> {
399 private PrintStream out;
400 private PrintStream err;
401 private int writeToErrLevel = Level.WARNING.getSeverity();
402
403 protected PrintStreamSubscriber() {
404 this(System.out, System.err);
405 }
406
407 protected PrintStreamSubscriber(PrintStream out, PrintStream err) {
408 this.out = out;
409 this.err = err;
410 }
411
412 private Level getLevel(Map<String, Serializable> logEntry) {
413 return (Level) logEntry.get(KEY_LEVEL);
414 }
415
416 @Override
417 public void onSubscribe(Subscription subscription) {
418 subscription.request(Long.MAX_VALUE);
419 }
420
421 @Override
422 public void onNext(Map<String, Serializable> item) {
423 if (getLevel(item).getSeverity() >= writeToErrLevel) {
424 err.print(toPrint(item));
425 } else {
426 out.print(toPrint(item));
427 }
428 // TODO flush for journald?
429 }
430
431 @Override
432 public void onError(Throwable throwable) {
433 throwable.printStackTrace(err);
434 }
435
436 @Override
437 public void onComplete() {
438 out.flush();
439 err.flush();
440 }
441
442 protected String firstLinePrefix(Map<String, Serializable> logEntry) {
443 Level level = getLevel(logEntry);
444 String spaces;
445 switch (level) {
446 case ERROR:
447 case DEBUG:
448 case TRACE:
449 spaces = " ";
450 break;
451 case INFO:
452 spaces = " ";
453 break;
454 case WARNING:
455 spaces = " ";
456 break;
457 case ALL:
458 spaces = " ";
459 break;
460 default:
461 throw new IllegalArgumentException("Unsupported level " + level);
462 }
463 return journald ? linePrefix(logEntry) : logEntry.get(KEY_INSTANT) + " " + level + spaces;
464 }
465
466 protected String firstLineSuffix(Map<String, Serializable> logEntry) {
467 return " - " + (logEntry.containsKey(KEY_CALL_LOCATION) ? logEntry.get(KEY_CALL_LOCATION)
468 : logEntry.get(KEY_LOGGER)) + " [" + logEntry.get(KEY_THREAD) + "]";
469 }
470
471 protected String linePrefix(Map<String, Serializable> logEntry) {
472 return journald ? "<" + levelToJournald(getLevel(logEntry)) + ">" : "";
473 }
474
475 protected int levelToJournald(Level level) {
476 int severity = level.getSeverity();
477 if (severity >= Level.ERROR.getSeverity())
478 return 3;
479 else if (severity >= Level.WARNING.getSeverity())
480 return 4;
481 else if (severity >= Level.INFO.getSeverity())
482 return 6;
483 else
484 return 7;
485 }
486
487 protected String toPrint(Map<String, Serializable> logEntry) {
488 StringBuilder sb = new StringBuilder();
489 StringTokenizer st = new StringTokenizer((String) logEntry.get(KEY_MSG), "\r\n");
490 assert st.hasMoreTokens();
491
492 // first line
493 String firstLine = st.nextToken();
494 sb.append(firstLinePrefix(logEntry));
495 sb.append(firstLine);
496 sb.append(firstLineSuffix(logEntry));
497 sb.append('\n');
498
499 // other lines
500 String prefix = linePrefix(logEntry);
501 while (st.hasMoreTokens()) {
502 sb.append(prefix);
503 sb.append(st.nextToken());
504 sb.append('\n');
505 }
506
507 if (logEntry.containsKey(KEY_THROWABLE)) {
508 Throwable throwable = (Throwable) logEntry.get(KEY_THROWABLE);
509 sb.append(prefix);
510 addThrowable(sb, prefix, throwable);
511 }
512 return sb.toString();
513 }
514
515 protected void addThrowable(StringBuilder sb, String prefix, Throwable throwable) {
516 sb.append(throwable.getClass().getName());
517 sb.append(": ");
518 sb.append(throwable.getMessage());
519 sb.append('\n');
520 for (StackTraceElement ste : throwable.getStackTrace()) {
521 sb.append(prefix);
522 sb.append(ste.toString());
523 sb.append('\n');
524 }
525 if (throwable.getCause() != null) {
526 sb.append(prefix);
527 sb.append("caused by ");
528 addThrowable(sb, prefix, throwable.getCause());
529 }
530 }
531 }
532
533 public static void main(String args[]) {
534 Logger logger = System.getLogger(ThinLogging.class.getName());
535 logger.log(Logger.Level.ALL, "Log all");
536 logger.log(Logger.Level.TRACE, "Multi\nline\ntrace");
537 logger.log(Logger.Level.DEBUG, "Log debug");
538 logger.log(Logger.Level.INFO, "Log info");
539 logger.log(Logger.Level.WARNING, "Log warning");
540 logger.log(Logger.Level.ERROR, "Log exception", new Throwable());
541 }
542
543 }