Naplózás

Szintén egy régi tartozásomat szeretném letudni, méghozzá a naplózó keretrendszerek ismertetését. Az egyik legrégebbi keretrendszer a Log4J, melyet Ceki Gülcü kezdett el fejleszteni. Bizonyos Apache projekteken belül az Apache Commons Logging volt elterjedt. Valamint bekerült naplózás a JDK-ba is, bár kicsit későn reagált az igényekre, a java.util.logging csomagba került, ezért gyakran hívjuk JUL-nak is. Mivel az eltérő library-k eltérő naplózó keretrendszereket definiáltak függőségként, ezért egy alkalmazás fejlesztésekor sajnos több naplózó rendszer is bekerült az alkalmazásba.

Erre a problémára megoldásként hozta létre szintén Ceki Gülcü az SLF4J keretrendszert, mely egy egyszerű API a különböző naplózó keretrendszerek elé. Van egy egyszerű implementációja is (SimpleLogger), de az összes többi keretrendszerhez van illesztése. Mindenképpen érdemes ezt használnunk, hiszen ekkor bármikor cserélhetjük alatta az implementációt.

A Log4J fejlesztése közben tanultak alapján alkotta meg Ceki Gülcü a következő keretrendszert, Logback néven.

Hogy még bonyolultabb legyen a helyzet, kijött a Log4J 2 is, melybe a Logbackből átemeltek pár dolgot, de úgy, hogy közben javítottak is rajta, olyan dolgokat, melyeket a Logbackben nem lehetett annak architektúrális megkötései miatt.

Ebben a posztban nézzük meg pár trükköt a naplózó keretrendszerek használatával kapcsolatban.

(Ennek kapcsán frissítettem egy régi Log4J cikkem, mely elérhető a GitHub-on, valamint van Log4J példa projekt is. A poszt további része természetesen a Logbacket is tárgyalja.)

SLF4J használata

Ahogy említettem, mindenképp érdemes az alkalmazásunkban az SLF4J-t használni, és a többi naplózó keretrendszeren nem függni. Ami mindenképpen szükséges, az a org.slf4j:slf4j-api jar a classpath-on, hiszen ebben találhatóak a Logger interfész és LoggerFactory osztály, melyet a kódunkban használunk. Az alkalmazást elindítva a következő hibát kaphatjuk:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Ez azt jelenti, hogy nincs implementáció, mely az API-t megvalósítaná. A legegyszerűbb, hogy a classpath-ba felvesszük még a org.slf4j:slf4j-simple jar-t is, azonban ez csak egy nagyon egyszerű naplózást tesz lehetővé, mely környezeti változókkal paraméterezhető.

A Logback natívan implementálja az SLF4J API-t, ezért ott külön trükközés nem szükséges, elegendő a ch.qos.logback:logback-classic (, és ennek függőségét, a ch.qos.logback:logback-core) jar-t elhelyezni a classpath-on.

A JUL esetén még egyszerűbb a helyzet, hiszen az a standard osztálykönyvtár része, ezért csak a org.slf4j: slf4j-jdk14) jar-t elhelyezni a classpath-on, ami nem más, mint egy adapter az SLF4J-hez, ami az SLF4J naplózás hívásokat a JUL-hoz továbbítja.

Ha azt akarjuk, hogy a Log4J legyen az SLF4J alatt, akkor egyrészt a Log4J jar-okat kell elhelyezni a classpath-on (log4j:log4j), valamint az SLF4J adaptert (org.slf4j:slf4j-log4j12).

A koncepcióról a következő ábra ad iránymutatást.

SLF4J API és implementációik

Függés több keretrendszeren

Sajnos előfordulhat olyan, hogy mégis több naplózó keretrendszer kerül be az alkalmazásunkba. Ez azért lehet, mert mi lehet, hogy csak SLF4J-t használunk, alatta Logback implementációval, az általunk használt library-k használhatnak más keretrendszereket is (pl. a Spring Apache Commons Logging keretrendszert használ).

Ilyenkor ún. bridge-eket kell használni. Tegyük fel, hogy az egyik keretrendszer hivatkozik a commons-logging:commons-logging jar-ra is, és az emiatt Apache Commons Logging keretrendszerrel próbál naplózni. Ilyenkor mind a két keretrendszert külön kéne konfigurálni. Emiatt azonban tehetjük azt, hogy az eredeti jar helyett, egy SLF4J bridge-et használunk (Apache Commons Logging esetén org.slf4j:jcl-over-slf4j). Ezen bridge azokat az osztályokat implementálja, melyek az eredeti jar-ban is szerepelnek (tehát azonos csomag és osztálynévvel), de a hívásokat az SLF4-hez továbbítja, ami meg az alatta lévő implementációnak. Azaz ebben az esetben annyi a feladatunk, hogy a naplózó keretrendszer jar-jait kivesszük az alkalmazásunkból, és betesszük a bridge-eket. Természetesen egy alkalmazásban több bridge-et is használhatunk. Amennyiben Mavent használunk, és az tranzitív függőségként húzza be a számunkra nem szükséges keretrendszert, akkor exclude-áljuk a pom.xml fájlban.

SLF4J bridge

Elvárások

A naplózással kapcsolatban a következő elvárásaim vannak:

  • Az alkalmazás forráskódját a fejlesztőeszközben megnyitva, és az alkalmazást futtatva azonnal legyen egy viszonylag részletes naplózás, mindenféle konfiguráció nélkül. Ahhoz, hogy a fejlesztőkörnyezet konzolján jelenjen meg, ennek a konzolra kell mennie.
  • Ugyanez igaz a tesztesetek futtatására is.
  • A Continuous Integration szerveren a tesztesetek naplózását felül lehessen bírálni.
  • Amennyiben az alkalmazást release-eljük, és telepítjük, legyen egy default naplózás, mindenféle konfiguráció nélkül.
  • A naplózást az alkalmazáson kívül is lehessen konfigurálni, így minden környezetre feltelepíthető legyen ugyanaz a jar/war/ear, és ne az alkalmazást kelljen a naplózáshoz módosítani.

Log4J konfiguráció felolvasása

A Log4J az előbb definiált igényeket alapból teljesíti. Az alkalmazás futtatáshoz tegyünk a classpath-ra egy log4j.properties állományt (pl. Maven esetén az src/main/resources könyvtárba) . A tesztesetek naplózásához tegyünk egy log4j.properties állományt a classpath-ra, és arra kell ügyelni, hogy előbb olvassa fel ezt. A Maven esetén szerencsére ez automatikusan megtörténik, azaz ha az src/test/resources könyvtárba teszünk egy log4j.properties állományt, azt fogja a Log4J betölteni.

Mind a CI szerver, mind a különböző környezetekben futó alkalmazások naplózását egyszerűen felül tudjuk bírálni rendszerváltozó megadásával, azaz a JVM-nek -D kapcsolóval meg kell adni a log4j.configuration változó értékeként egy konfigurációs állomány elérési útvonalát. Ezt a file: előtaggal kell megtennünk, mivel ellenkező esetben a classpath-on fogja más elérési útvonalon keresni.

Azaz pl. a következő paranccsal indítsuk az alkalmazásunk:

java -Dlog4j.configuration=file:/home/jtechlog/Documents/jtechlog-log4j-sample/target/log4j.properties \
  -jar jtechlog-log4j-sample-1.0.0-SNAPSHOT-jar-with-dependencies.jar

Windows esetén szükség van a meghajtóra is, azaz pl. file:/c:/log4j.properties.

Amennyiben a naplózás nem úgy történik, ahogy beállítottuk, annak az lehet az okat, hogy több jarban is szerepel log4j.properties állomány, és nem azt olvasta be, amelyiket szerettük volna. Ekkor a -Dlog4j.debug kapcsoló megadásával nyomon követhetjük, hogyan történik a beolvasás. Ekkor valami ilyen üzenetet kaphatunk:

log4j: Trying to find [log4j.xml] using context classloader sun.misc.Launcher$AppClassLoader@55f96302.
log4j: Trying to find [log4j.xml] using sun.misc.Launcher$AppClassLoader@55f96302 class loader.
log4j: Trying to find [log4j.xml] using ClassLoader.getSystemResource().
log4j: Trying to find [log4j.properties] using context classloader sun.misc.Launcher$AppClassLoader@55f96302.
log4j: Using URL [jar:file:/home/jtechlog/Documents/jtechlog-log4j-sample/target/jtechlog-log4j-sample-1.0.0-SNAPSHOT-jar-with-dependencies.jar!/log4j.properties] for automatic log4j configuration.
log4j: Reading configuration from URL jar:file:/home/jtechlog/Documents/jtechlog-log4j-sample/target/jtechlog-log4j-sample-1.0.0-SNAPSHOT-jar-with-dependencies.jar!/log4j.properties

Logback konfiguráció felolvasása

A Logback hasonlóképp működik, mint a Log4J. Annyi a különbség, hogy előbb a logback-test.xml állományt, majd a logback.xml állományt próbálja beolvasni. Ezért a fő ágon, és a teszt ágon lévő konfigurációs fájlok neve is eltérhet. Valamint a konfigurációs fájl helyét a -Dlogback.configurationFile kapcsolóval lehet megadni, valamint a Logback 1.0.4 verziójától kezdve használható a -Dlogback.debug kapcsoló is.

Spring naplózás

A Spring fejlesztésének elején úgy döntöttek, hogy az Apache Commons Logging keretrendszert választják naplózáshoz. Azóta ez rossz döntésnek bizonyult, és a helyes megoldás, ha saját alkalmazásunkban SLF4J-t használunk, és a Spring naplózását is ebbe vezetjük. A fentebb leírt módszer alapján, ha Mavent használunk ehhez először ki kell exclude-álnunk a commons-logging:commons-logging függőséget a org.springframework:spring-core függőségnél, és bevezetni a org.slf4j:slf4j-api függőséget, valamint a org.slf4j:jcl-over-slf4j függőséget a Spring napló üzeneteinek SLF4J keretrendszerbe irányításához. Aztán tegyünk alá egy implementációt, pl. Logback esetén a ch.qos.logback:logback-classic (, és ennek függőségét, a ch.qos.logback:logback-core) függőséget. Erről bővebben a referencia Logging fejezete ír.

Használjuk a Maven mvn dependency:tree parancsát, hogy meggyőződjünk arról, hogy nem kerül be más naplózó keretrendszer a classpath-ra. Ha igen, akkor exclude-áljuk.

Spring Boot naplózás

A Spring Boot naplózás konfigurációjáról a referencia Logging fejezete ír. Használható a JUL, Log4J 2 és Logback is. Amennyiben startert használunk, a Logback lesz használva, és a megfelelő bridge-ek is a classpath-on vannak. Alapból info szinten naplóz a konzolra, de ezt a --debug kapcsolóval, vagy a debug=true property beállításával (pl. application.properties állományban) debug szintre tudjuk állítani. A Twelve-Factor App ajánlás szerint csak konzolra naplózzunk, de amennyiben fájlba is szeretnénk, használjuk a logging.file vagy logging.path property-t (ide is info szinten naplóz, és 10 MB után rotálja a napló állományt). Ha még jobban szeretnénk konfigurálni, akkor a classpath-on javasolt elhelyezni egy logback-spring.xml állományt, vagy a logging.config property segítségével beállíthatjuk, hogy hol van a konfigurációs állomány.

A Logback tökéletes választás, de amennyiben más naplózó keretrendszert szeretnénk használni, deklaráljuk függőségként, valamint használjuk a org.springframework.boot.logging.LoggingSystem környezeti változót. Ennek paraméterül egy LoggingSystem interfészt implementáló osztály minősített nevét kell megadni.