Dynamische Logfiles mit Log4j

mniemann

Grünschnabel
Hallo,

folgende Frage:

Ich benutze log4j zum loggen der Applikation. Dieses funktioniert soweit auch prima. Nun habe ich allerdings die Anforderung, dass für jeden User, der die Applikation nutzt ein eigenes Logfile geschrieben werden soll, damit die parallel geschriebenen Logeinträge besser sichtbar in einzelne Logfiles abgelegt werden.

Ausschnit aus der log4j.xml:
<appender name="COMMON" class="org.apache.log4j.RollingFileAppender">
<param name="File" value="${jboss.server.log.dir}/common/common.log" />
<param name="Append" value="true" />
<param name="MaxFileSize" value="1024KB" />
<param name="MaxBackupIndex" value="3" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{dd MMM yyy HH:mm:ss} %-5p %m%n" />
</layout>
</appender>

Mit anderen Worten, derzeit wird alles in die Datei common.log geschrieben. Es sollen allerdings z.B.: common_user1.log oder common_user2.log angelegt werden. In der Applikation soll der Username an log4j übergeben werden, damit dann das entsprechende File angelegt werden kann.

Ist dieses mit log4j möglich? Wenn ja, gibt es dafür Beispiele (Ich habe bisher leider keine finden können).

Danke!
 
Leider weiß ich es auch nicht, aber es interessiert mich ebenfalls brennend.
(leider weiß ich immernoch nicht wie man Themen abonieren kann, ohne hier was loszu werden)
 
Hallo,

dazu gibts mehrere Möglichkeiten... Stichworte sind hierzu:
Nested Diagnostic Context (NDC) and Mapped Diagnostic Context (MDC)
Die hier gezeigte Ansatz besteht aus einem speziellen Appender welcher an andere Appender delegiert. An welchen Appender nun delegiert werden soll (welche Variante angesprochen werden soll) wird dem Appender auf "irgendeine" Weise mitgeteilt (in meinem beispiel verwende ich eine ThreadLocal Variable - hiermit sage ich dass jede Aktion eines bestimmten User in einem user-eigenen Thread abläuft, das ist natürlich nur eine Möglichkeit von vielen...) Viel eleganter lässt sich das hier mit einem Aspect realisieren.

Der Code ist natürlich prototypisch und soll nur das Prinzip darstellen.

Java:
/**
 * 
 */
package de.tutorials;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;

/**
 * @author Thomas.Darimont
 * 
 */
public class Log4JLoggingExample {
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		new Thread() {
			@Override
			public void run() {
				MDC.put("USERNAME", "foo");
				Logger logger = Logger.getLogger(Log4JLoggingExample.class);
				logger.log(Level.INFO, MDC.get("USERNAME") + "Bubu");
			}
		}.start();

		new Thread() {
			@Override
			public void run() {
				MDC.put("USERNAME", "bar");
				Logger logger = Logger.getLogger(Log4JLoggingExample.class);
				logger.log(Level.INFO, MDC.get("USERNAME") + "Bubu");
			}
		}.start();

		Logger.getLogger(Log4JLoggingExample.class).log(Level.INFO, "bubu");

	}

}

Java:
/**
 * 
 */
package de.tutorials;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.spi.LoggingEvent;

/**
 * @author Thomas.Darimont
 * 
 */
public class VariantAwareDelegatingRollingFileAppender extends
		RollingFileAppender {
	ConcurrentMap<String, RollingFileAppender> variantToAppenderMap = new ConcurrentHashMap<String, RollingFileAppender>();

	String variantKey;

	public String getVariantKey() {
		return variantKey;
	}

	public void setVariantKey(String variant) {
		this.variantKey = variant;
	}

	@Override
	public void append(LoggingEvent event) {
		Object value = MDC.get(variantKey);
		if (value != null) {
			String variant = String.valueOf(value);
			RollingFileAppender appender = getAppenderDelegatee(variant);
			appender.append(event);
		} else {
			super.append(event);
		}
	}

	private RollingFileAppender getAppenderDelegatee(String variant) {
		RollingFileAppender appender = variantToAppenderMap.get(variant);
		if (appender == null) {
			StringBuilder stringBuilder = new StringBuilder(getFile());
			stringBuilder.insert(stringBuilder.indexOf("."), "_" + variant);
			try {
				appender = new RollingFileAppender(getLayout(),
						stringBuilder.toString());
				appender.setMaxBackupIndex(getMaxBackupIndex());
				appender.setMaximumFileSize(getMaximumFileSize());
				variantToAppenderMap.putIfAbsent(variant, appender);
			} catch (IOException e) {
				Logger.getLogger(getClass()).log(Level.ERROR,
						"Could not create Logger for variant: " + variant,
						e);
			}
		}
		return appender;
	}

	@Override
	public synchronized void close() {
		super.close();

		for (String variant : variantToAppenderMap.keySet()) {
			try {
				variantToAppenderMap.get(variant).close();
			} catch (Exception e) {
				e.printStackTrace();
				// ignore
			}
		}

		variantToAppenderMap.clear();

	}
}

hier noch die log4j.properties:
Code:
log4j.rootLogger=INFO, logfile
log4j.appender.logfile=de.tutorials.VariantAwareDelegatingRollingFileAppender
log4j.appender.logfile.variantKey=USERNAME
log4j.appender.logfile.File=logs/activity.log
log4j.appender.logfile.MaxFileSize=512KB
log4j.appender.logfile.MaxBackupIndex=3
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] [%X{USERNAME}]- %m%n

Dadurch werden im Verzeichnis logs drei Log Dateien erzeugt:
activity.log:
activity_bar.log
activity_foo.log

Gruß Tom
 
Hej,

vielen lieben Dank für Deine Antwort und sorry, dass ich mich erst jetzt wieder melde. Aber ich habe nun einige Zeit damit verbracht, mein Problem zu lösen. Dein Ansatz mit dem eigenen Appender ist sehr gut. Nur leider sind Threads in EJBs nicht erlaubt. Unsere Architektur ist allerdings so aufgebaut. Ich bin noch andere Möglichkeiten durchgegangen, aber letztlich immer wieder an kleinen Sachen gescheitert. Welche Möglichkeiten gibt es ausser ThreadLocal sonst noch, um den Usernamen in den Appender zu bekommen?

Sonnige Grüße!
 
Hallo,

in meinem Beispiel verwende erzeuge ich diese Threads nur zu Demonstrationszwecken. Es ist laut JEE EJB Spec nicht erlaubt eigene Threads innerhalb einer Managed Umgebung wie ein EJB Container zu erzeugen. Das heißt aber nicht das da intern keine Threads verwendet werden...

Je nach Anwendungsszenario / kontext kann man das so ohne Probleme / bzw. mit einer leichten Modifikation verwenden. Dazu braucht man aber mehr Informationen darüber, wie du den Code ausführst / aufrufst.

Rufst du die Methoden an der EJB von einer Webanwendung (also colocated / in der selben JVM) aus oder von einem Remote Client auf ?
Wo / wie werden denn der aktuelle Prozesszustand und die aktuellen Benutzerinformationen gespeichert?

Gruß Tom
 
Hallo,
Deine erste Lösung klappt zwar sehr gut, ist allerdings - wie Du auch erwähnt hast - in unserer EJB nicht zu verwenden (da Threads). Ich habe also versucht, den Usernamen (der von uns durch die komplette Applikation durchgeschliffen wird) irgendwie an den eigenen Appender weiterzureichen. Das hat irgendwie nicht geklappt (vielleicht bin ich allerdings auch zu doof dafür). Also habe ich mir noch weitere Lösungsansätze durchprogrammiert (Appender nehmen und mit getFile() und setFile den Dateinamen verändern...dies hat allerdings auch nur bedingt funktioniert (da der Dateiname ja komplett neu gespeichert wird und somit neue User den neuen Dateinamen bekommen und nicht den ursprünglichen).
Wir haben ne Webapplikation, die verschiedene Bereiche abdeckt. Alle Bereiche haben eigene Business Components (EJBs). Die Webapplikation läuft im Tomcat, die Business-Logik im JBoss.
Deinen Ansatz mit den eigenen Appendern find ich sehr gut....nur ist halt bisher keine Möglichkeit eingefallen, wie ich auf den Usernamen zugreifen kann. Dieser wird über die Log-Aufrufe (eigene Wrapper-Klasse für Log4j) weitergereicht.

Gruß,
Markus
 
Hallo,

gib doch mal im Servlet Thread.currentThread().getName() aus.
Das selbe nochmal in der EJB Komponente...
sind das unterschiedliche Threads oder die gleichen?

Wenns (IMMER) die gleichen sind, dann kannst du die Username Informationen auch über den MDC weitergeben. In der Regel wird hier nämlich der selbe thread weiterverwendet, der für das Bearbeiten des HTTP Requests verantwortlich ist.

Wenn du dir also in deinem Servlet mit
@EJB
private NewSessionLocal newSessionBean;

beispielsweise eine SLSB injecten lässt und dann deine Businessmethode aufrufst bist du immer noch im Kontext des HTTP-Worker-threads.

Eine weitere Möglichkeit wäre es die Informationen in einem MBean "quasi" Singleton abzulegen.

Das funktioniert natürlich nur solange wie man das threading nicht umkonfiguriert, bzw. anfängt die Anwendungslogik auf mehrere Rechner zu verteilen (Clustering). Hier muss man sich dann andere Wege suchen. Einer wäre dann beispielsweise sowas wie ne "Enterprise HashMap" á La TerraCotta, Oracle Coherence oder Open Spaces. Damit kann man dann aber schon ein wenig mehr machen ;-)

Gruß Tom
 
Moin,
wow....eigentlich sollte das nur ein kleines Feature unserer Applikation sein. Die Geschichte mit den Servlets t bei uns nicht so gut. Wir setzen JSF (IceFaces) ein. Allerdings haben nicht alle Applikationen/Prozesse ein Webfrontend. Es werden ebenso Prozesse in Abhängigkeit von anderen gestartet. Daher haben wir uns hier auf JMS und MessageDrivenBeans zur Übergabe von diversen Parametern (u.a. auch Username) entschieden. Genauso gut kann es sein, dass ein Batch-Job gestartet wird. Wir haben eine Log EJB, die wiederum eine Logger-Wrapper-Klasse anspricht. Gibt es denn keine einfache Möglichkeit den Usernamen zu übergeben?

Gruß,
Markus

P.S.: Vielen Dank für Deine Mühe!
 
Ok, letztendlich war es dann doch nicht so schwer.....Die Geschichte mit dem eigenen Appender und MDC klappt bislang sehr gut. Auch habe ich mit den Threads innerhalb der EJB nun keine Probleme.

Vielen lieben Dank für Deine Hilfe.
 
Guten Tag.
Habe nicht exakt das gleiche Problem, aber evtl. weiß jemand bescheid.
Ich habe ein großes Projekt, in dem nicht nur die Web-Anwendung drin steckt, sondern auch die Applets.
Jetzt habe ich eine log4j.xml erstellt, die wunderbar funktioniert. Ist es jetzt möglich dem Logger in den Applets eine andere log4j.xml zuzuteilen?
Irgendwie so auf diese Art:
Code:
private static Logger log = Logger.getLogger("/nix/temp/log4j.xml");
Weiß jemand Rat?
Vielen Dank!

Gruß
Gerrit
 
Zurück