Skip to content

Commit

Permalink
Implementation & tests for SLF4J flogger backend
Browse files Browse the repository at this point in the history
Closes #61

RELNOTES=Adds a new `slf4j-backend-factory` artifact

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=238273653
  • Loading branch information
cslee00 authored and ronshapiro committed Mar 13, 2019
1 parent ac95fe8 commit 2a67023
Show file tree
Hide file tree
Showing 6 changed files with 469 additions and 2 deletions.
1 change: 1 addition & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package_group(
"//api/...",
"//google/...",
"//log4j/...",
"//slf4j/...",
"//tools/...",
],
)
4 changes: 2 additions & 2 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "google_bazel_common",
strip_prefix = "bazel-common-0a14592ccc7d6018643d841ee61a29ae3e9d4684",
urls = ["https://github.com/google/bazel-common/archive/0a14592ccc7d6018643d841ee61a29ae3e9d4684.zip"],
strip_prefix = "bazel-common-f1115e0f777f08c3cdb115526c4e663005bec69b",
urls = ["https://github.com/google/bazel-common/archive/f1115e0f777f08c3cdb115526c4e663005bec69b.zip"],
)

load("@google_bazel_common//:workspace_defs.bzl", "google_common_workspace_rules")
Expand Down
55 changes: 55 additions & 0 deletions slf4j/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2018 Google Inc. All Rights Reserved.
#
# Description:
# Flogger slf4j backend (google.github.io/flogger).

package(default_visibility = ["//:internal"])

SLF4J_BACKEND_SRCS = glob(["src/main/java/**/*.java"])

java_library(
name = "slf4j_backend",
srcs = SLF4J_BACKEND_SRCS,
javacopts = ["-source 1.6 -target 1.6"],
tags = ["maven_coordinates=com.google.flogger:flogger-slf4j-backend:${project.version}"],
deps = [
"//api",
"//api:system_backend",
"@google_bazel_common//third_party/java/slf4j_api",
],
)

load("//tools:maven.bzl", "pom_file")

pom_file(
name = "pom",
artifact_id = "flogger-slf4j-backend",
artifact_name = "Flogger SLF4J Backend",
targets = [":slf4j_backend"],
)

load("@google_bazel_common//tools/javadoc:javadoc.bzl", "javadoc_library")

javadoc_library(
name = "javadoc",
srcs = SLF4J_BACKEND_SRCS,
root_packages = ["com.google.common.flogger.backend.slf4j"],
deps = [":slf4j_backend"],
)

# ---- Unit Tests ----

load("@google_bazel_common//testing:test_defs.bzl", "gen_java_tests")

gen_java_tests(
name = "slf4j_tests",
srcs = glob(["src/test/java/**/*.java"]),
deps = [
":slf4j_backend",
"//api",
"//api:testing",
"@google_bazel_common//third_party/java/junit",
"@google_bazel_common//third_party/java/mockito",
"@google_bazel_common//third_party/java/slf4j_api",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 The Flogger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.common.flogger.backend.slf4j;

import com.google.common.flogger.backend.LoggerBackend;
import com.google.common.flogger.backend.system.BackendFactory;
import org.slf4j.LoggerFactory;

/**
* BackendFactory for SLF4J.
*
* <p>To configure this backend for Flogger set the following system property (also see {@link
* com.google.common.flogger.backend.system.DefaultPlatform}):
*
* <ul>
* <li>{@code flogger.backend_factory=
* com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance}.
* </ul>
*/
public final class Slf4jBackendFactory extends BackendFactory {

private static final Slf4jBackendFactory INSTANCE = new Slf4jBackendFactory();

/** This method is expected to be called via reflection (and might otherwise be unused). */
public static BackendFactory getInstance() {
return INSTANCE;
}

@Override
public LoggerBackend create(String loggingClassName) {
return new Slf4jLoggerBackend(LoggerFactory.getLogger(loggingClassName.replace('$', '.')));
}

@Override
public String toString() {
return "SLF4J backend";
}

private Slf4jBackendFactory() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright (C) 2018 The Flogger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.common.flogger.backend.slf4j;

import com.google.common.flogger.backend.LogData;
import com.google.common.flogger.backend.LoggerBackend;
import com.google.common.flogger.backend.Metadata;
import com.google.common.flogger.backend.SimpleMessageFormatter;
import java.util.logging.Level;
import org.slf4j.Logger;

/** A logging backend that uses slf4j to output log statements. */
final class Slf4jLoggerBackend extends LoggerBackend
implements SimpleMessageFormatter.SimpleLogHandler {

private final Logger logger;

Slf4jLoggerBackend(Logger logger) {
if (logger == null) {
throw new NullPointerException("logger is required");
}
this.logger = logger;
}

// represents the log levels supported by SLF4J, used internally to dispatch calls accordingly
private enum Slf4jLogLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR
}

/**
* Adapts the JUL level to SLF4J level per the below mapping:
*
* <table>
* <tr>
* <th>JUL</th>
* <th>SLF4J</th>
* </tr>
* <tr>
* <td>FINEST</td><td>TRACE</td>
* </tr><tr>
* <td>FINER</td><td>TRACE</td>
* </tr>
* <tr>
* <td>FINE</td><td>DEBUG</td>
* </tr>
* <tr>
* <td>CONFIG</td><td>DEBUG</td>
* </tr>
* <tr>
* <td>INFO</td>
* <td>INFO</td>
* </tr>
* <tr>
* <td>WARNING</td>
* <td>WARN</td>
* </tr>
* <tr>
* <td>SEVERE</td>
* <td>ERROR</td>
* </tr>
* </table>
*
* <p>Custom JUL levels are mapped to the next-lowest standard JUL level; for example, a custom
* level at 750 (between INFO:800 and CONFIG:700) would map to the same as CONFIG (DEBUG).
*
* <p>It isn't expected that the JUL levels 'ALL' or 'OFF' are passed into this method; doing so
* will throw an IllegalArgumentException, as those levels are for configuration, not logging
*
* @param level the JUL level to map; any standard or custom JUL level, except for ALL or OFF
* @return the MappedLevel object representing the SLF4J adapters appropriate for the requested
* log level; never null.
*/
private static Slf4jLogLevel mapToSlf4jLogLevel(Level level) {
// Performance consideration: mapToSlf4jLogLevel is a very hot method, called even when
// logging is disabled. Allocations (and other latency-introducing constructs) should be avoided
int requestedLevel = level.intValue();

// Flogger shouldn't allow ALL or OFF to be used for logging
// if Flogger does add this check to the core library it can be removed here (and should be,
// as this method is on the critical performance path for determining whether log statements
// are disabled, hence called for all log statements)
if (requestedLevel == Level.ALL.intValue() || requestedLevel == Level.OFF.intValue()) {
throw new IllegalArgumentException("Unsupported log level: " + level);
}

if (requestedLevel < Level.FINE.intValue()) {
return Slf4jLogLevel.TRACE;
}

if (requestedLevel < Level.INFO.intValue()) {
return Slf4jLogLevel.DEBUG;
}

if (requestedLevel < Level.WARNING.intValue()) {
return Slf4jLogLevel.INFO;
}

if (requestedLevel < Level.SEVERE.intValue()) {
return Slf4jLogLevel.WARN;
}

return Slf4jLogLevel.ERROR;
}

@Override
public String getLoggerName() {
return logger.getName();
}

@Override
public boolean isLoggable(Level level) {
// Performance consideration: isLoggable is a very hot method, called even when logging is
// disabled. Allocations (and any other latency-introducing constructs) should be avoided
Slf4jLogLevel slf4jLogLevel = mapToSlf4jLogLevel(level);

// dispatch to each level-specific method as SLF4J doesn't expose a logger.log( level, ... )
switch (slf4jLogLevel) {
case TRACE:
return logger.isTraceEnabled();
case DEBUG:
return logger.isDebugEnabled();
case INFO:
return logger.isInfoEnabled();
case WARN:
return logger.isWarnEnabled();
case ERROR:
return logger.isErrorEnabled();
}
throw new AssertionError("Unknown SLF4J log level: " + slf4jLogLevel);
}

@Override
public void log(LogData data) {
SimpleMessageFormatter.format(data, this);
}

@Override
public void handleError(RuntimeException exception, LogData badData) {
StringBuilder errorMsg = new StringBuilder(200);
errorMsg.append("LOGGING ERROR: ");
errorMsg.append(exception.getMessage());
errorMsg.append('\n');
formatBadLogData(badData, errorMsg);

// log at ERROR to ensure visibility
logger.error(errorMsg.toString(), exception);
}

// based on com.google.common.flogger.backend.system.AbstractLogRecord.safeAppend
private static void formatBadLogData(LogData data, StringBuilder out) {
out.append(" original message: ");
if (data.getTemplateContext() == null) {
out.append(data.getLiteralArgument());
} else {
// We know that there's at least one argument to display here.
out.append(data.getTemplateContext().getMessage());
out.append("\n original arguments:");
for (Object arg : data.getArguments()) {
out.append("\n ").append(SimpleMessageFormatter.safeToString(arg));
}
}
Metadata metadata = data.getMetadata();
if (metadata.size() > 0) {
out.append("\n metadata:");
for (int n = 0; n < metadata.size(); n++) {
out.append("\n ");
out.append(metadata.getKey(n).getLabel()).append(": ").append(metadata.getValue(n));
}
}
out.append("\n level: ").append(data.getLevel());
out.append("\n timestamp (nanos): ").append(data.getTimestampNanos());
out.append("\n class: ").append(data.getLogSite().getClassName());
out.append("\n method: ").append(data.getLogSite().getMethodName());
out.append("\n line number: ").append(data.getLogSite().getLineNumber());
}

@Override
public void handleFormattedLogMessage(Level level, String message, Throwable thrown) {
Slf4jLogLevel slf4jLogLevel = mapToSlf4jLogLevel(level);

// dispatch to each level-specific method, as SLF4J doesn't expose a logger.log( level, ... )
switch (slf4jLogLevel) {
case TRACE:
logger.trace(message, thrown);
return;
case DEBUG:
logger.debug(message, thrown);
return;
case INFO:
logger.info(message, thrown);
return;
case WARN:
logger.warn(message, thrown);
return;
case ERROR:
logger.error(message, thrown);
return;
}
throw new AssertionError("Unknown SLF4J log level: " + slf4jLogLevel);
}
}
Loading

4 comments on commit 2a67023

@PascalSchumacher
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very nice to see this merged. 👍

It would be great to have a release containing this feature!

@ronshapiro
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :)

@PascalSchumacher
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much! :)

@hagbard
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, and sorry for the delay. I've been too busy to look at this myself due to other (non logging) project stuff, and Ron's been "holding the fort" for release stuff. I'll try and ensure things don't take as long in future :/

Please sign in to comment.