Flogger Fluent Logging

1. Übersicht

In diesem Tutorial werden wir über das Flogger-Framework sprechen, eine von Google entwickelte flüssige Protokollierungs-API für Java.

2. Warum Flogger verwenden?

Warum brauchen wir bei all den derzeit auf dem Markt befindlichen Protokollierungsframeworks wie Log4j und Logback noch ein weiteres Protokollierungsframework?

Es stellt sich heraus, dass Flogger gegenüber anderen Frameworks mehrere Vorteile hat - werfen wir einen Blick darauf.

2.1. Lesbarkeit

Die fließende Art der Flogger-API trägt wesentlich dazu bei, dass sie besser lesbar ist.

Schauen wir uns ein Beispiel an, in dem alle zehn Iterationen eine Nachricht protokolliert werden soll.

Mit einem traditionellen Protokollierungsframework würden wir Folgendes sehen:

int i = 0; // ... if (i % 10 == 0) { logger.info("This log shows every 10 iterations"); i++; }

Mit Flogger kann das oben Gesagte jedoch vereinfacht werden, um:

logger.atInfo().every(10).log("This log shows every 10 iterations");

Während man argumentieren würde, dass die Flogger-Version der Logger-Anweisung etwas ausführlicher aussieht als die herkömmlichen Versionen, erlaubt sie eine größere Funktionalität und führt letztendlich zu lesbareren und aussagekräftigeren Protokollanweisungen .

2.2. Performance

Protokollierungsobjekte werden optimiert, solange wir vermeiden, für die protokollierten Objekte toString aufzurufen :

User user = new User(); logger.atInfo().log("The user is: %s", user);

Wenn wir wie oben gezeigt protokollieren, hat das Backend die Möglichkeit, die Protokollierung zu optimieren. Auf der anderen Seite, wenn wir nennen toString direkt oder verketten die Saiten dann wird diese Gelegenheit verloren:

logger.atInfo().log("Ths user is: %s", user.toString()); logger.atInfo().log("Ths user is: %s" + user);

2.3. Erweiterbarkeit

Das Flogger-Framework deckt bereits die meisten grundlegenden Funktionen ab, die wir von einem Protokollierungsframework erwarten würden.

Es gibt jedoch Fälle, in denen wir die Funktionalität erweitern müssten. In diesen Fällen ist es möglich, die API zu erweitern.

Derzeit ist hierfür eine separate unterstützende Klasse erforderlich. Wir könnten zum Beispiel die Flogger-API erweitern, indem wir eine UserLogger- Klasse schreiben :

logger.at(INFO).forUserId(id).withUsername(username).log("Message: %s", param);

Dies kann in Fällen hilfreich sein, in denen die Nachricht konsistent formatiert werden soll. Der UserLogger würde dann die Implementierung für die benutzerdefinierten Methoden forUserId (String-ID) und withUsername (String-Benutzername) bereitstellen.

Dazu muss die UserLogger- Klasse die AbstractLogger- Klasse erweitern und eine Implementierung für die API bereitstellen . Wenn wir uns FluentLogger ansehen , handelt es sich nur um einen Logger ohne zusätzliche Methoden. Daher können wir diese Klasse zunächst unverändert kopieren und dann auf dieser Grundlage aufbauen, indem wir ihr Methoden hinzufügen.

2.4. Effizienz

Traditionelle Frameworks verwenden häufig Varargs. Für diese Methoden muss ein neues Objekt [] zugewiesen und gefüllt werden, bevor die Methode aufgerufen werden kann. Darüber hinaus müssen alle übergebenen Grundtypen automatisch verpackt werden.

Dies alles kostet zusätzlichen Bytecode und Latenz am Anrufort. Es ist besonders bedauerlich, wenn die Protokollanweisung nicht aktiviert ist. Die Kosten werden in Protokollen auf Debug-Ebene deutlicher, die häufig in Schleifen angezeigt werden. Flogger spart diese Kosten, indem er Varargs vollständig vermeidet.

Flogger umgeht dieses Problem mithilfe einer fließenden Aufrufkette, aus der Protokollierungsanweisungen erstellt werden können. Auf diese Weise kann das Framework nur eine geringe Anzahl von Überschreibungen für die Protokollmethode vornehmen und so Dinge wie Varargs und Auto-Boxing vermeiden. Dies bedeutet, dass die API eine Vielzahl neuer Funktionen ohne kombinatorische Explosion aufnehmen kann.

Ein typisches Protokollierungsframework verfügt über folgende Methoden:

level(String, Object) level(String, Object...)

Dabei kann die Ebene einer von etwa sieben Namen auf Protokollebene sein ( z. B. schwerwiegend ) und über eine kanonische Protokollmethode verfügen, die eine zusätzliche Protokollebene akzeptiert:

log(Level, Object...)

Darüber hinaus gibt es normalerweise Varianten der Methoden, die eine Ursache (eine Throwable- Instanz) haben, die der Protokollanweisung zugeordnet ist:

level(Throwable, String, Object) level(Throwable, String, Object...)

Es ist klar, dass die API drei Probleme in einem Methodenaufruf zusammenfasst:

  1. Es wird versucht, die Protokollebene anzugeben (Methodenauswahl).
  2. Versuch, Metadaten an die Protokollanweisung anzuhängen (auslösbare Ursache)
  3. Außerdem geben Sie die Protokollnachricht und die Argumente an.

Dieser Ansatz multipliziert schnell die Anzahl der verschiedenen Protokollierungsmethoden, die erforderlich sind, um diese unabhängigen Bedenken zu erfüllen.

Wir können jetzt sehen, warum es wichtig ist, zwei Methoden in der Kette zu haben:

logger.atInfo().withCause(e).log("Message: %s", arg);

Schauen wir uns nun an, wie wir es in unserer Codebasis verwenden können.

3. Abhängigkeiten

Es ist ziemlich einfach, Flogger einzurichten. Wir müssen nur Peitsche und Peitschersystem-Backend zu unserem Pom hinzufügen :

  com.google.flogger flogger 0.4   com.google.flogger flogger-system-backend 0.4 runtime  

Nachdem diese Abhängigkeiten eingerichtet wurden, können wir nun die API untersuchen, die uns zur Verfügung steht.

4. Erkunden der Fluent API

Lassen Sie uns zunächst eine statische Instanz für unseren Logger deklarieren :

private static final FluentLogger logger = FluentLogger.forEnclosingClass();

Und jetzt können wir mit der Protokollierung beginnen. Wir beginnen mit etwas Einfachem:

int result = 45 / 3; logger.atInfo().log("The result is %d", result);

Die Log - Meldungen können alle Java verwenden printf Formatbezeich wie % s,% d oder 016x% .

4.1. Vermeiden von Arbeiten an Protokollstandorten

Flogger-Ersteller empfehlen, keine Arbeiten an der Protokollseite durchzuführen.

Let's say we have the following long-running method for summarising the current state of a component:

public static String collectSummaries() { longRunningProcess(); int items = 110; int s = 30; return String.format("%d seconds elapsed so far. %d items pending processing", s, items); }

It's tempting to call collectSummaries directly in our log statement:

logger.atFine().log("stats=%s", collectSummaries());

Regardless of the configured log levels or rate-limiting, though, the collectSummaries method will now be called every time.

Making the cost of disabled logging statements virtually free is at the core of the logging framework. This, in turn, means that more of them can be left intact in the code without harm. Writing the log statement like we just did takes away this advantage.

Instead, we should do use the LazyArgs.lazy method:

logger.atFine().log("stats=%s", LazyArgs.lazy(() -> collectSummaries()));

Now, almost no work is done at the log site — just instance creation for the lambda expression. Flogger will only evaluate this lambda if it intends to actually log the message.

Although it's allowed to guard log statements using isEnabled:

if (logger.atFine().isEnabled()) { logger.atFine().log("summaries=%s", collectSummaries()); }

This is not necessary and we should avoid it because Flogger does these checks for us. This approach also only guards log statements by level and does not help with rate-limited log statements.

4.2. Dealing With Exceptions

How about exceptions, how do we handle them?

Well, Flogger comes with a withStackTrace method that we can use to log a Throwable instance:

try { int result = 45 / 0; } catch (RuntimeException re) { logger.atInfo().withStackTrace(StackSize.FULL).withCause(re).log("Message"); }

Where withStackTrace takes as an argument the StackSize enum with constant values SMALL, MEDIUM, LARGE or FULL. A stack trace generated by withStackTrace() will show up as a LogSiteStackTrace exception in the default java.util.logging backend. Other backends may choose to handle this differently though.

4.3. Logging Configuration and Levels

So far we've been using logger.atInfo in most of our examples, but Flogger does support many other levels. We'll look at these, but first, let's introduce how to configure the logging options.

To configure logging, we use the LoggerConfig class.

For example, when we want to set the logging level to FINE:

LoggerConfig.of(logger).setLevel(Level.FINE);

And Flogger supports various logging levels:

logger.atInfo().log("Info Message"); logger.atWarning().log("Warning Message"); logger.atSevere().log("Severe Message"); logger.atFine().log("Fine Message"); logger.atFiner().log("Finer Message"); logger.atFinest().log("Finest Message"); logger.atConfig().log("Config Message");

4.4. Rate Limiting

How about the issue of rate-limiting? How do we handle the case where we don't want to log every iteration?

Flogger comes to our rescue with the every(int n) method:

IntStream.range(0, 100).forEach(value -> { logger.atInfo().every(40).log("This log shows every 40 iterations => %d", value); });

We get the following output when we run the code above:

Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0 INFO: This log shows every 40 iterations => 0 [CONTEXT ratelimit_count=40 ] Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0 INFO: This log shows every 40 iterations => 40 [CONTEXT ratelimit_count=40 ] Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0 INFO: This log shows every 40 iterations => 80 [CONTEXT ratelimit_count=40 ]

What if we want to log say every 10 seconds? Then, we can use atMostEvery(int n, TimeUnit unit):

IntStream.range(0, 1_000_0000).forEach(value -> { logger.atInfo().atMostEvery(10, TimeUnit.SECONDS).log("This log shows [every 10 seconds] => %d", value); });

With this, the outcome now becomes:

Sep 18, 2019 5:08:06 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1 INFO: This log shows [every 10 seconds] => 0 [CONTEXT ratelimit_period="10 SECONDS" ] Sep 18, 2019 5:08:16 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1 INFO: This log shows [every 10 seconds] => 3545373 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3545372]" ] Sep 18, 2019 5:08:26 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1 INFO: This log shows [every 10 seconds] => 7236301 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3690927]" ]

5. Using Flogger With Other Backends

So, what if we would like to add Flogger to our existing application that is already using say Slf4j or Log4j for example? This could be useful in cases where we would want to take advantage of our existing configurations. Flogger supports multiple backends as we'll see.

5.1 Flogger With Slf4j

It's simple to configure an Slf4j back-end. First, we need to add the flogger-slf4j-backend dependency to our pom:

 com.google.flogger flogger-slf4j-backend 0.4 

Next, we need to tell Flogger that we would like to use a different back-end from the default one. We do this by registering a Flogger factory through system properties:

System.setProperty( "flogger.backend_factory", "com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance");

And now our application will use the existing configuration.

5.1 Flogger With Log4j

Wir folgen ähnlichen Schritten zum Konfigurieren des Log4j-Backends. Lassen Sie uns das Hinzufügen flogger-log4j-Back - End - Abhängigkeit unserer pom :

 com.google.flogger flogger-log4j-backend 0.4   com.sun.jmx jmxri   com.sun.jdmk jmxtools   javax.jms jms     log4j log4j 1.2.17   log4j apache-log4j-extras 1.2.17 

Wir müssen auch eine Flogger-Backend-Factory für Log4j registrieren:

System.setProperty( "flogger.backend_factory", "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");

Und das war's, unsere Anwendung ist jetzt so eingerichtet, dass sie vorhandene Log4j-Konfigurationen verwendet!

6. Fazit

In diesem Tutorial haben wir gesehen, wie das Flogger-Framework als Alternative zu den herkömmlichen Protokollierungsframeworks verwendet wird. Wir haben einige leistungsstarke Funktionen gesehen, von denen wir bei der Verwendung des Frameworks profitieren können.

Wir haben auch gesehen, wie wir unsere vorhandenen Konfigurationen nutzen können, indem wir verschiedene Back-Ends wie Slf4j und Log4j registrieren.

Wie üblich ist der Quellcode für dieses Tutorial auf GitHub verfügbar.