]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.init/src/org/argeo/init/logging/ThinLogging.java
Use latest release of Argeo Maven parent
[lgpl/argeo-commons.git] / org.argeo.init / src / org / 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 // 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();
295 switch (className) {
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;
309 continue stack;
310 default:
311 }
312 }
313 if (stack.length > lowestLoggerInterface + 1)
314 callLocation = stack[lowestLoggerInterface + 1];
315 }
316 return callLocation;
317 }
318
319 }
320
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();
328
329 private class LogEntryPublisher extends SubmissionPublisher<Map<String, Serializable>> {
330
331 private LogEntryPublisher(Executor executor, int maxBufferCapacity) {
332 super(executor, maxBufferCapacity);
333 }
334
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;
339 assert msg != null;
340 assert instant != null;
341 assert thread != null;
342
343 final long sequence = nextEntry.incrementAndGet();
344
345 Map<String, Serializable> logEntry = new LogEntryMap(sequence);
346
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);
351 if (thrown != null)
352 logEntry.put(KEY_THROWABLE, thrown);
353 if (callLocation != null)
354 logEntry.put(KEY_CALL_LOCATION, callLocation);
355
356 // object is a string
357 logEntry.put(KEY_LOGGER, logger.getName());
358 logEntry.put(KEY_THREAD, thread.getName());
359
360 // should be unmodifiable for security reasons
361 submit(Collections.unmodifiableMap(logEntry));
362 }
363
364 }
365
366 /**
367 * An internal optimisation for collections. It should not be referred to
368 * directly as a type.
369 */
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;
374
375 private final long sequence;
376
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
381 super(10, 0.75f);
382 this.sequence = sequence;
383 }
384
385 @Override
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));
392 } else
393 return false;
394 }
395
396 @Override
397 public int hashCode() {
398 return (int) sequence;
399 }
400
401 }
402
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();
407
408 protected PrintStreamSubscriber() {
409 this(System.out, System.err);
410 }
411
412 protected PrintStreamSubscriber(PrintStream out, PrintStream err) {
413 this.out = out;
414 this.err = err;
415 }
416
417 private Level getLevel(Map<String, Serializable> logEntry) {
418 return (Level) logEntry.get(KEY_LEVEL);
419 }
420
421 @Override
422 public void onSubscribe(Subscription subscription) {
423 subscription.request(Long.MAX_VALUE);
424 }
425
426 @Override
427 public void onNext(Map<String, Serializable> item) {
428 if (getLevel(item).getSeverity() >= writeToErrLevel) {
429 err.print(toPrint(item));
430 } else {
431 out.print(toPrint(item));
432 }
433 // TODO flush for journald?
434 }
435
436 @Override
437 public void onError(Throwable throwable) {
438 throwable.printStackTrace(err);
439 }
440
441 @Override
442 public void onComplete() {
443 out.flush();
444 err.flush();
445 }
446
447 protected String firstLinePrefix(Map<String, Serializable> logEntry) {
448 Level level = getLevel(logEntry);
449 String spaces;
450 switch (level) {
451 case ERROR:
452 case DEBUG:
453 case TRACE:
454 spaces = " ";
455 break;
456 case INFO:
457 spaces = " ";
458 break;
459 case WARNING:
460 spaces = " ";
461 break;
462 case ALL:
463 spaces = " ";
464 break;
465 default:
466 throw new IllegalArgumentException("Unsupported level " + level);
467 }
468 return journald ? linePrefix(logEntry) : logEntry.get(KEY_INSTANT) + " " + level + spaces;
469 }
470
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) + "]";
474 }
475
476 protected String linePrefix(Map<String, Serializable> logEntry) {
477 return journald ? "<" + levelToJournald(getLevel(logEntry)) + ">" : "";
478 }
479
480 protected int levelToJournald(Level level) {
481 int severity = level.getSeverity();
482 if (severity >= Level.ERROR.getSeverity())
483 return 3;
484 else if (severity >= Level.WARNING.getSeverity())
485 return 4;
486 else if (severity >= Level.INFO.getSeverity())
487 return 6;
488 else
489 return 7;
490 }
491
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();
496
497 // first line
498 String firstLine = st.nextToken();
499 sb.append(firstLinePrefix(logEntry));
500 sb.append(firstLine);
501 sb.append(firstLineSuffix(logEntry));
502 sb.append('\n');
503
504 // other lines
505 String prefix = linePrefix(logEntry);
506 while (st.hasMoreTokens()) {
507 sb.append(prefix);
508 sb.append(st.nextToken());
509 sb.append('\n');
510 }
511
512 if (logEntry.containsKey(KEY_THROWABLE)) {
513 Throwable throwable = (Throwable) logEntry.get(KEY_THROWABLE);
514 sb.append(prefix);
515 addThrowable(sb, prefix, throwable);
516 }
517 return sb.toString();
518 }
519
520 protected void addThrowable(StringBuilder sb, String prefix, Throwable throwable) {
521 sb.append(throwable.getClass().getName());
522 sb.append(": ");
523 sb.append(throwable.getMessage());
524 sb.append('\n');
525 for (StackTraceElement ste : throwable.getStackTrace()) {
526 sb.append(prefix);
527 sb.append('\t');
528 sb.append(ste.toString());
529 sb.append('\n');
530 }
531 if (throwable.getCause() != null) {
532 sb.append(prefix);
533 sb.append("Caused by: ");
534 addThrowable(sb, prefix, throwable.getCause());
535 }
536 }
537 }
538
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());
547 }
548
549 }