]> 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.api.cms.CmsLog":
301 case "org.slf4j.impl.ArgeoLogger":
302 case "org.eclipse.jetty.util.log.Slf4jLog":
303 case "sun.util.logging.internal.LoggingProviderImpl$JULWrapper":
304 lowestLoggerInterface = i;
305 continue stack;
306 default:
307 }
308 }
309 if (stack.length > lowestLoggerInterface + 1)
310 callLocation = stack[lowestLoggerInterface + 1];
311 }
312 return callLocation;
313 }
314
315 }
316
317 private final static String KEY_LOGGER = Logger.class.getName();
318 private final static String KEY_LEVEL = Level.class.getName();
319 private final static String KEY_MSG = String.class.getName();
320 private final static String KEY_THROWABLE = Throwable.class.getName();
321 private final static String KEY_INSTANT = Instant.class.getName();
322 private final static String KEY_CALL_LOCATION = StackTraceElement.class.getName();
323 private final static String KEY_THREAD = Thread.class.getName();
324
325 private class LogEntryPublisher extends SubmissionPublisher<Map<String, Serializable>> {
326
327 private LogEntryPublisher(Executor executor, int maxBufferCapacity) {
328 super(executor, maxBufferCapacity);
329 }
330
331 private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant,
332 Thread thread, Throwable thrown, StackTraceElement callLocation) {
333 assert level != null;
334 assert logger != null;
335 assert msg != null;
336 assert instant != null;
337 assert thread != null;
338
339 final long sequence = nextEntry.incrementAndGet();
340
341 Map<String, Serializable> logEntry = new LogEntryMap(sequence);
342
343 // same object as key class name
344 logEntry.put(KEY_LEVEL, level);
345 logEntry.put(KEY_MSG, msg);
346 logEntry.put(KEY_INSTANT, instant);
347 if (thrown != null)
348 logEntry.put(KEY_THROWABLE, thrown);
349 if (callLocation != null)
350 logEntry.put(KEY_CALL_LOCATION, callLocation);
351
352 // object is a string
353 logEntry.put(KEY_LOGGER, logger.getName());
354 logEntry.put(KEY_THREAD, thread.getName());
355
356 // should be unmodifiable for security reasons
357 submit(Collections.unmodifiableMap(logEntry));
358 }
359
360 }
361
362 /**
363 * An internal optimisation for collections. It should not be referred to
364 * directly as a type.
365 */
366 // TODO optimise memory with a custom map implementation?
367 // but access may be slower
368 private static class LogEntryMap extends HashMap<String, Serializable> {
369 private static final long serialVersionUID = 7361434381922521356L;
370
371 private final long sequence;
372
373 private LogEntryMap(long sequence) {
374 // maximum 7 fields, so using default load factor 0.75
375 // an initial size of 10 should prevent rehashing (7 / 0.75 ~ 9.333)
376 // see HashMap class description for more details
377 super(10, 0.75f);
378 this.sequence = sequence;
379 }
380
381 @Override
382 public boolean equals(Object o) {
383 if (o instanceof LogEntryMap)
384 return sequence == ((LogEntryMap) o).sequence;
385 else if (o instanceof Map) {
386 Map<?, ?> map = (Map<?, ?>) o;
387 return get(KEY_INSTANT).equals(map.get(KEY_INSTANT)) && get(KEY_THREAD).equals(map.get(KEY_THREAD));
388 } else
389 return false;
390 }
391
392 @Override
393 public int hashCode() {
394 return (int) sequence;
395 }
396
397 }
398
399 private class PrintStreamSubscriber implements Flow.Subscriber<Map<String, Serializable>> {
400 private PrintStream out;
401 private PrintStream err;
402 private int writeToErrLevel = Level.WARNING.getSeverity();
403
404 protected PrintStreamSubscriber() {
405 this(System.out, System.err);
406 }
407
408 protected PrintStreamSubscriber(PrintStream out, PrintStream err) {
409 this.out = out;
410 this.err = err;
411 }
412
413 private Level getLevel(Map<String, Serializable> logEntry) {
414 return (Level) logEntry.get(KEY_LEVEL);
415 }
416
417 @Override
418 public void onSubscribe(Subscription subscription) {
419 subscription.request(Long.MAX_VALUE);
420 }
421
422 @Override
423 public void onNext(Map<String, Serializable> item) {
424 if (getLevel(item).getSeverity() >= writeToErrLevel) {
425 err.print(toPrint(item));
426 } else {
427 out.print(toPrint(item));
428 }
429 // TODO flush for journald?
430 }
431
432 @Override
433 public void onError(Throwable throwable) {
434 throwable.printStackTrace(err);
435 }
436
437 @Override
438 public void onComplete() {
439 out.flush();
440 err.flush();
441 }
442
443 protected String firstLinePrefix(Map<String, Serializable> logEntry) {
444 Level level = getLevel(logEntry);
445 String spaces;
446 switch (level) {
447 case ERROR:
448 case DEBUG:
449 case TRACE:
450 spaces = " ";
451 break;
452 case INFO:
453 spaces = " ";
454 break;
455 case WARNING:
456 spaces = " ";
457 break;
458 case ALL:
459 spaces = " ";
460 break;
461 default:
462 throw new IllegalArgumentException("Unsupported level " + level);
463 }
464 return journald ? linePrefix(logEntry) : logEntry.get(KEY_INSTANT) + " " + level + spaces;
465 }
466
467 protected String firstLineSuffix(Map<String, Serializable> logEntry) {
468 return " - " + (logEntry.containsKey(KEY_CALL_LOCATION) ? logEntry.get(KEY_CALL_LOCATION)
469 : logEntry.get(KEY_LOGGER)) + " [" + logEntry.get(KEY_THREAD) + "]";
470 }
471
472 protected String linePrefix(Map<String, Serializable> logEntry) {
473 return journald ? "<" + levelToJournald(getLevel(logEntry)) + ">" : "";
474 }
475
476 protected int levelToJournald(Level level) {
477 int severity = level.getSeverity();
478 if (severity >= Level.ERROR.getSeverity())
479 return 3;
480 else if (severity >= Level.WARNING.getSeverity())
481 return 4;
482 else if (severity >= Level.INFO.getSeverity())
483 return 6;
484 else
485 return 7;
486 }
487
488 protected String toPrint(Map<String, Serializable> logEntry) {
489 StringBuilder sb = new StringBuilder();
490 StringTokenizer st = new StringTokenizer((String) logEntry.get(KEY_MSG), "\r\n");
491 assert st.hasMoreTokens();
492
493 // first line
494 String firstLine = st.nextToken();
495 sb.append(firstLinePrefix(logEntry));
496 sb.append(firstLine);
497 sb.append(firstLineSuffix(logEntry));
498 sb.append('\n');
499
500 // other lines
501 String prefix = linePrefix(logEntry);
502 while (st.hasMoreTokens()) {
503 sb.append(prefix);
504 sb.append(st.nextToken());
505 sb.append('\n');
506 }
507
508 if (logEntry.containsKey(KEY_THROWABLE)) {
509 Throwable throwable = (Throwable) logEntry.get(KEY_THROWABLE);
510 sb.append(prefix);
511 addThrowable(sb, prefix, throwable);
512 }
513 return sb.toString();
514 }
515
516 protected void addThrowable(StringBuilder sb, String prefix, Throwable throwable) {
517 sb.append(throwable.getClass().getName());
518 sb.append(": ");
519 sb.append(throwable.getMessage());
520 sb.append('\n');
521 for (StackTraceElement ste : throwable.getStackTrace()) {
522 sb.append(prefix);
523 sb.append('\t');
524 sb.append(ste.toString());
525 sb.append('\n');
526 }
527 if (throwable.getCause() != null) {
528 sb.append(prefix);
529 sb.append("Caused by: ");
530 addThrowable(sb, prefix, throwable.getCause());
531 }
532 }
533 }
534
535 public static void main(String args[]) {
536 Logger logger = System.getLogger(ThinLogging.class.getName());
537 logger.log(Logger.Level.ALL, "Log all");
538 logger.log(Logger.Level.TRACE, "Multi\nline\ntrace");
539 logger.log(Logger.Level.DEBUG, "Log debug");
540 logger.log(Logger.Level.INFO, "Log info");
541 logger.log(Logger.Level.WARNING, "Log warning");
542 logger.log(Logger.Level.ERROR, "Log exception", new Throwable());
543 }
544
545 }