]> git.argeo.org Git - lgpl/argeo-commons.git/blob - kernel/NodeLogger.java
Prepare next development cycle
[lgpl/argeo-commons.git] / kernel / NodeLogger.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.argeo.cms.internal.kernel;
17
18 import java.io.IOException;
19 import java.net.URI;
20 import java.nio.file.FileSystems;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import java.nio.file.StandardWatchEventKinds;
24 import java.nio.file.WatchEvent;
25 import java.nio.file.WatchKey;
26 import java.nio.file.WatchService;
27 import java.security.SignatureException;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Enumeration;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.ListIterator;
36 import java.util.Map;
37 import java.util.Properties;
38 import java.util.concurrent.BlockingQueue;
39 import java.util.concurrent.LinkedBlockingQueue;
40
41 import org.apache.commons.logging.Log;
42 import org.apache.commons.logging.LogFactory;
43 import org.apache.log4j.AppenderSkeleton;
44 import org.apache.log4j.Level;
45 import org.apache.log4j.LogManager;
46 import org.apache.log4j.Logger;
47 import org.apache.log4j.PropertyConfigurator;
48 import org.apache.log4j.spi.LoggingEvent;
49 import org.argeo.cms.CmsException;
50 import org.argeo.cms.auth.CurrentUser;
51 import org.argeo.node.ArgeoLogListener;
52 import org.argeo.node.ArgeoLogger;
53 import org.argeo.node.NodeConstants;
54 import org.argeo.osgi.useradmin.UserAdminConf;
55 import org.osgi.framework.Bundle;
56 import org.osgi.framework.Constants;
57 import org.osgi.framework.ServiceReference;
58 import org.osgi.service.cm.ConfigurationAdmin;
59 import org.osgi.service.log.LogEntry;
60 import org.osgi.service.log.LogListener;
61 import org.osgi.service.log.LogReaderService;
62 import org.osgi.service.log.LogService;
63
64 /** Not meant to be used directly in standard log4j config */
65 class NodeLogger implements ArgeoLogger, LogListener {
66 /** Internal debug for development purposes. */
67 private static Boolean debug = false;
68
69 // private final static Log log = LogFactory.getLog(NodeLogger.class);
70
71 private Boolean disabled = false;
72
73 private String level = null;
74
75 private Level log4jLevel = null;
76 // private Layout layout;
77
78 private Properties configuration;
79
80 private AppenderImpl appender;
81
82 private final List<ArgeoLogListener> everythingListeners = Collections
83 .synchronizedList(new ArrayList<ArgeoLogListener>());
84 private final List<ArgeoLogListener> allUsersListeners = Collections
85 .synchronizedList(new ArrayList<ArgeoLogListener>());
86 private final Map<String, List<ArgeoLogListener>> userListeners = Collections
87 .synchronizedMap(new HashMap<String, List<ArgeoLogListener>>());
88
89 private BlockingQueue<LogEvent> events;
90 private LogDispatcherThread logDispatcherThread = new LogDispatcherThread();
91
92 private Integer maxLastEventsCount = 10 * 1000;
93
94 /** Marker to prevent stack overflow */
95 private ThreadLocal<Boolean> dispatching = new ThreadLocal<Boolean>() {
96
97 @Override
98 protected Boolean initialValue() {
99 return false;
100 }
101 };
102
103 @SuppressWarnings("unchecked")
104 public NodeLogger(LogReaderService lrs) {
105 Enumeration<LogEntry> logEntries = lrs.getLog();
106 while (logEntries.hasMoreElements())
107 logged(logEntries.nextElement());
108 lrs.addLogListener(this);
109
110 // configure log4j watcher
111 String log4jConfiguration = KernelUtils.getFrameworkProp("log4j.configuration");
112 if (log4jConfiguration != null && log4jConfiguration.startsWith("file:")) {
113 if (log4jConfiguration.contains("..")) {
114 if (log4jConfiguration.startsWith("file://"))
115 log4jConfiguration = log4jConfiguration.substring("file://".length());
116 else if (log4jConfiguration.startsWith("file:"))
117 log4jConfiguration = log4jConfiguration.substring("file:".length());
118 }
119 try {
120 Path log4jconfigPath;
121 if (log4jConfiguration.startsWith("file:"))
122 log4jconfigPath = Paths.get(new URI(log4jConfiguration));
123 else
124 log4jconfigPath = Paths.get(log4jConfiguration);
125 Thread log4jConfWatcher = new Log4jConfWatcherThread(log4jconfigPath);
126 log4jConfWatcher.start();
127 } catch (Exception e) {
128 stdErr("Badly formatted log4j configuration URI " + log4jConfiguration + ": " + e.getMessage());
129 }
130 }
131 }
132
133 public void init() {
134 try {
135 events = new LinkedBlockingQueue<LogEvent>();
136
137 // if (layout != null)
138 // setLayout(layout);
139 // else
140 // setLayout(new PatternLayout(pattern));
141 appender = new AppenderImpl();
142 reloadConfiguration();
143 Logger.getRootLogger().addAppender(appender);
144
145 logDispatcherThread = new LogDispatcherThread();
146 logDispatcherThread.start();
147 } catch (Exception e) {
148 throw new CmsException("Cannot initialize log4j");
149 }
150 }
151
152 public void destroy() throws Exception {
153 Logger.getRootLogger().removeAppender(appender);
154 allUsersListeners.clear();
155 for (List<ArgeoLogListener> lst : userListeners.values())
156 lst.clear();
157 userListeners.clear();
158
159 events.clear();
160 events = null;
161 logDispatcherThread.interrupt();
162 }
163
164 // public void setLayout(Layout layout) {
165 // this.layout = layout;
166 // }
167
168 public String toString() {
169 return "Node Logger";
170 }
171
172 //
173 // OSGi LOGGER
174 //
175 @Override
176 public void logged(LogEntry status) {
177 Log pluginLog = LogFactory.getLog(status.getBundle().getSymbolicName());
178 Integer severity = status.getLevel();
179 if (severity == LogService.LOG_ERROR) {
180 // FIXME Fix Argeo TP
181 if (status.getException() instanceof SignatureException)
182 return;
183 pluginLog.error(msg(status), status.getException());
184 } else if (severity == LogService.LOG_WARNING)
185 pluginLog.warn(msg(status), status.getException());
186 else if (severity == LogService.LOG_INFO && pluginLog.isDebugEnabled())
187 pluginLog.debug(msg(status), status.getException());
188 else if (severity == LogService.LOG_DEBUG && pluginLog.isTraceEnabled())
189 pluginLog.trace(msg(status), status.getException());
190 }
191
192 private String msg(LogEntry status) {
193 StringBuilder sb = new StringBuilder();
194 sb.append(status.getMessage());
195 Bundle bundle = status.getBundle();
196 if (bundle != null) {
197 sb.append(" '" + bundle.getSymbolicName() + "'");
198 }
199 ServiceReference<?> sr = status.getServiceReference();
200 if (sr != null) {
201 sb.append(' ');
202 String[] objectClasses = (String[]) sr.getProperty(Constants.OBJECTCLASS);
203 if (isSpringApplicationContext(objectClasses)) {
204 sb.append("{org.springframework.context.ApplicationContext}");
205 Object symbolicName = sr.getProperty(Constants.BUNDLE_SYMBOLICNAME);
206 if (symbolicName != null)
207 sb.append(" " + Constants.BUNDLE_SYMBOLICNAME + ": " + symbolicName);
208 } else {
209 sb.append(arrayToString(objectClasses));
210 }
211 Object cn = sr.getProperty(NodeConstants.CN);
212 if (cn != null)
213 sb.append(" " + NodeConstants.CN + ": " + cn);
214 Object factoryPid = sr.getProperty(ConfigurationAdmin.SERVICE_FACTORYPID);
215 if (factoryPid != null)
216 sb.append(" " + ConfigurationAdmin.SERVICE_FACTORYPID + ": " + factoryPid);
217 // else {
218 // Object servicePid = sr.getProperty(Constants.SERVICE_PID);
219 // if (servicePid != null)
220 // sb.append(" " + Constants.SERVICE_PID + ": " + servicePid);
221 // }
222 // servlets
223 Object whiteBoardPattern = sr.getProperty(KernelConstants.WHITEBOARD_PATTERN_PROP);
224 if (whiteBoardPattern != null) {
225 if (whiteBoardPattern instanceof String) {
226 sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": " + whiteBoardPattern);
227 } else {
228 sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": "
229 + arrayToString((String[]) whiteBoardPattern));
230 }
231 }
232 // RWT
233 Object contextName = sr.getProperty(KernelConstants.CONTEXT_NAME_PROP);
234 if (contextName != null)
235 sb.append(" " + KernelConstants.CONTEXT_NAME_PROP + ": " + contextName);
236
237 // user directories
238 Object baseDn = sr.getProperty(UserAdminConf.baseDn.name());
239 if (baseDn != null)
240 sb.append(" " + UserAdminConf.baseDn.name() + ": " + baseDn);
241
242 }
243 return sb.toString();
244 }
245
246 private String arrayToString(Object[] arr) {
247 StringBuilder sb = new StringBuilder();
248 sb.append('[');
249 for (int i = 0; i < arr.length; i++) {
250 if (i != 0)
251 sb.append(',');
252 sb.append(arr[i]);
253 }
254 sb.append(']');
255 return sb.toString();
256 }
257
258 private boolean isSpringApplicationContext(String[] objectClasses) {
259 for (String clss : objectClasses) {
260 if (clss.equals("org.eclipse.gemini.blueprint.context.DelegatedExecutionOsgiBundleApplicationContext")) {
261 return true;
262 }
263 }
264 return false;
265 }
266
267 //
268 // ARGEO LOGGER
269 //
270
271 public synchronized void register(ArgeoLogListener listener, Integer numberOfPreviousEvents) {
272 String username = CurrentUser.getUsername();
273 if (username == null)
274 throw new CmsException("Only authenticated users can register a log listener");
275
276 if (!userListeners.containsKey(username)) {
277 List<ArgeoLogListener> lst = Collections.synchronizedList(new ArrayList<ArgeoLogListener>());
278 userListeners.put(username, lst);
279 }
280 userListeners.get(username).add(listener);
281 List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(username, numberOfPreviousEvents);
282 for (LogEvent evt : lastEvents)
283 dispatchEvent(listener, evt);
284 }
285
286 public synchronized void registerForAll(ArgeoLogListener listener, Integer numberOfPreviousEvents,
287 boolean everything) {
288 if (everything)
289 everythingListeners.add(listener);
290 else
291 allUsersListeners.add(listener);
292 List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(null, numberOfPreviousEvents);
293 for (LogEvent evt : lastEvents)
294 if (everything || evt.getUsername() != null)
295 dispatchEvent(listener, evt);
296 }
297
298 public synchronized void unregister(ArgeoLogListener listener) {
299 String username = CurrentUser.getUsername();
300 if (username == null)// FIXME
301 return;
302 if (!userListeners.containsKey(username))
303 throw new CmsException("No user listeners " + listener + " registered for user " + username);
304 if (!userListeners.get(username).contains(listener))
305 throw new CmsException("No user listeners " + listener + " registered for user " + username);
306 userListeners.get(username).remove(listener);
307 if (userListeners.get(username).isEmpty())
308 userListeners.remove(username);
309
310 }
311
312 public synchronized void unregisterForAll(ArgeoLogListener listener) {
313 everythingListeners.remove(listener);
314 allUsersListeners.remove(listener);
315 }
316
317 /** For development purpose, since using regular logging is not easy here */
318 private static void stdOut(Object obj) {
319 System.out.println(obj);
320 }
321
322 private static void stdErr(Object obj) {
323 System.err.println(obj);
324 }
325
326 private static void debug(Object obj) {
327 if (debug)
328 System.out.println(obj);
329 }
330
331 private static boolean isInternalDebugEnabled() {
332 return debug;
333 }
334
335 // public void setPattern(String pattern) {
336 // this.pattern = pattern;
337 // }
338
339 public void setDisabled(Boolean disabled) {
340 this.disabled = disabled;
341 }
342
343 public void setLevel(String level) {
344 this.level = level;
345 }
346
347 public void setConfiguration(Properties configuration) {
348 this.configuration = configuration;
349 }
350
351 public void updateConfiguration(Properties configuration) {
352 setConfiguration(configuration);
353 reloadConfiguration();
354 }
355
356 public Properties getConfiguration() {
357 return configuration;
358 }
359
360 /**
361 * Reloads configuration (if the configuration {@link Properties} is set)
362 */
363 protected void reloadConfiguration() {
364 if (configuration != null) {
365 LogManager.resetConfiguration();
366 PropertyConfigurator.configure(configuration);
367 }
368 }
369
370 protected synchronized void processLoggingEvent(LogEvent event) {
371 if (disabled)
372 return;
373
374 if (dispatching.get())
375 return;
376
377 if (level != null && !level.trim().equals("")) {
378 if (log4jLevel == null || !log4jLevel.toString().equals(level))
379 try {
380 log4jLevel = Level.toLevel(level);
381 } catch (Exception e) {
382 System.err.println("Log4j level could not be set for level '" + level + "', resetting it to null.");
383 e.printStackTrace();
384 level = null;
385 }
386
387 if (log4jLevel != null && !event.getLoggingEvent().getLevel().isGreaterOrEqual(log4jLevel)) {
388 return;
389 }
390 }
391
392 try {
393 // admin listeners
394 Iterator<ArgeoLogListener> everythingIt = everythingListeners.iterator();
395 while (everythingIt.hasNext())
396 dispatchEvent(everythingIt.next(), event);
397
398 if (event.getUsername() != null) {
399 Iterator<ArgeoLogListener> allUsersIt = allUsersListeners.iterator();
400 while (allUsersIt.hasNext())
401 dispatchEvent(allUsersIt.next(), event);
402
403 if (userListeners.containsKey(event.getUsername())) {
404 Iterator<ArgeoLogListener> userIt = userListeners.get(event.getUsername()).iterator();
405 while (userIt.hasNext())
406 dispatchEvent(userIt.next(), event);
407 }
408 }
409 } catch (Exception e) {
410 stdOut("Cannot process logging event");
411 e.printStackTrace();
412 }
413 }
414
415 protected void dispatchEvent(ArgeoLogListener logListener, LogEvent evt) {
416 LoggingEvent event = evt.getLoggingEvent();
417 logListener.appendLog(evt.getUsername(), event.getTimeStamp(), event.getLevel().toString(),
418 event.getLoggerName(), event.getThreadName(), event.getMessage(), event.getThrowableStrRep());
419 }
420
421 private class AppenderImpl extends AppenderSkeleton {
422 public boolean requiresLayout() {
423 return false;
424 }
425
426 public void close() {
427 }
428
429 @Override
430 protected void append(LoggingEvent event) {
431 if (events != null) {
432 try {
433 String username = CurrentUser.getUsername();
434 events.put(new LogEvent(username, event));
435 } catch (InterruptedException e) {
436 // silent
437 }
438 }
439 }
440
441 }
442
443 private class LogDispatcherThread extends Thread {
444 /** encapsulated in order to simplify concurrency management */
445 private LinkedList<LogEvent> lastEvents = new LinkedList<LogEvent>();
446
447 public LogDispatcherThread() {
448 super("Argeo Logging Dispatcher Thread");
449 }
450
451 public void run() {
452 while (events != null) {
453 try {
454 LogEvent loggingEvent = events.take();
455 processLoggingEvent(loggingEvent);
456 addLastEvent(loggingEvent);
457 } catch (InterruptedException e) {
458 if (events == null)
459 return;
460 }
461 }
462 }
463
464 protected synchronized void addLastEvent(LogEvent loggingEvent) {
465 if (lastEvents.size() >= maxLastEventsCount)
466 lastEvents.poll();
467 lastEvents.add(loggingEvent);
468 }
469
470 public synchronized List<LogEvent> getLastEvents(String username, Integer maxCount) {
471 LinkedList<LogEvent> evts = new LinkedList<LogEvent>();
472 ListIterator<LogEvent> it = lastEvents.listIterator(lastEvents.size());
473 int count = 0;
474 while (it.hasPrevious() && (count < maxCount)) {
475 LogEvent evt = it.previous();
476 if (username == null || username.equals(evt.getUsername())) {
477 evts.push(evt);
478 count++;
479 }
480 }
481 return evts;
482 }
483 }
484
485 private class LogEvent {
486 private final String username;
487 private final LoggingEvent loggingEvent;
488
489 public LogEvent(String username, LoggingEvent loggingEvent) {
490 super();
491 this.username = username;
492 this.loggingEvent = loggingEvent;
493 }
494
495 @Override
496 public int hashCode() {
497 return loggingEvent.hashCode();
498 }
499
500 @Override
501 public boolean equals(Object obj) {
502 return loggingEvent.equals(obj);
503 }
504
505 @Override
506 public String toString() {
507 return username + "@ " + loggingEvent.toString();
508 }
509
510 public String getUsername() {
511 return username;
512 }
513
514 public LoggingEvent getLoggingEvent() {
515 return loggingEvent;
516 }
517
518 }
519
520 private class Log4jConfWatcherThread extends Thread {
521 private Path log4jConfigurationPath;
522
523 public Log4jConfWatcherThread(Path log4jConfigurationPath) {
524 super("Log4j Configuration Watcher");
525 try {
526 this.log4jConfigurationPath = log4jConfigurationPath.toRealPath();
527 } catch (IOException e) {
528 this.log4jConfigurationPath = log4jConfigurationPath.toAbsolutePath();
529 stdOut("Cannot determine real path for " + log4jConfigurationPath + ": " + e.getMessage());
530 }
531 }
532
533 public void run() {
534 Path parentDir = log4jConfigurationPath.getParent();
535 try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
536 parentDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
537 WatchKey wk;
538 watching: while ((wk = watchService.take()) != null) {
539 for (WatchEvent<?> event : wk.pollEvents()) {
540 final Path changed = (Path) event.context();
541 if (log4jConfigurationPath.equals(parentDir.resolve(changed))) {
542 if (isInternalDebugEnabled())
543 debug(log4jConfigurationPath + " has changed, reloading.");
544 PropertyConfigurator.configure(log4jConfigurationPath.toUri().toURL());
545 }
546 }
547 // reset the key
548 boolean valid = wk.reset();
549 if (!valid) {
550 break watching;
551 }
552 }
553 } catch (IOException | InterruptedException e) {
554 stdErr("Log4j configuration watcher failed: " + e.getMessage());
555 }
556 }
557 }
558 }