Skip to content

benkard/quarkus-googlecloud-jsonlogging

Repository files navigation

Quarkus Google Cloud JSON Logging

Structured logging to standard output according to the Google Cloud Logging specification.

1. Summary

This package contains a log formatter for JBoss Logging in the form of a Quarkus plugin that implements the Google Cloud Logging JSON format on standard output.

It is possible to log unstructured text, structured data, or a mixture of both depending on the situation.

2. Activation (Quarkus)

Add the runtime POM to your dependency list. As long as the JAR is on the classpath at both build time and runtime, the log formatter automatically registers itself on startup.

If you are using Maven:

<dependencies>

  <dependency>
    <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
    <artifactId>quarkus-googlecloud-jsonlogging</artifactId>
    <version>6.6.0</version>
  </dependency>

</dependencies>

If you are using Gradle:

dependencies {
  implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging:6.6.0")
}

By default the extension is turned on. You can turn the extension on or off explicitly by configuring the quarkus.log.console.google key in application.properties:

quarkus.log.console.google = true

3. Activation (Other Frameworks)

If you are not using Quarkus, you can still make use of the -core module and wire it into your application in a custom way. Read this section for hints on how to do so.

3.1. Installation

If you are using Maven:

<dependencies>

  <dependency>
    <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
    <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
    <version>6.6.0</version>
  </dependency>

</dependencies>

If you are using Gradle:

dependencies {
  implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:6.6.0")
}

3.2. Wiring (java.util.logging)

This section explains how to use JBoss Log Manager as the log manager for java.util.logging and how to configure it to use the log formatter provided by this library.

Add a dependency on JBoss Log Manager and the -core module to your project.

If you are using Maven:

<dependencies>

  <dependency>
    <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
    <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
    <version>6.6.0</version>
  </dependency>

  <dependency>
    <groupId>org.jboss.logmanager</groupId>
    <artifactId>jboss-logmanager</artifactId>
    <version>3.0.2.Final</version>
  </dependency>

</dependencies>

If you are using Gradle:

dependencies {
  implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:6.6.0")
  implementation("org.jboss.logmanager:jboss-logmanager:3.0.2.Final")
}

Create a text file called org.jboss.logmanager.ConfiguratorFactory in your resources/META-INF/services/ directory and put the fully qualified name of this library’s DefaultConfiguratorFactory into it:

eu.mulk.quarkus.googlecloud.jsonlogging.logmanager.DefaultConfiguratorFactory

Finally, set the java.util.logging.manager system property to org.jboss.logmanager.LogManager when running your application:

$ java -Djava.util.logging.manager=org.jboss.logmanager.LogManager ...

3.3. Wiring (Spring Boot)

If you are using Spring Boot, the easiest way to integrate the log formatter is by relying on spring-boot-starter-logging (which is pulled in by spring-boot-starter), excluding Logback, and pulling in JBoss Log Manager as the back end for SLF4J. In addition, configure JBoss Log Manager as the log manager for java.util.logging by setting the java.util.logging.manager system property to org.jboss.logmanager.LogManager.

If you are using Maven:

<project>

  <dependencies>

    <dependency>
      <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
      <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
      <version>6.6.0</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.slf4j</groupId>
      <artifactId>slf4j-jboss-logmanager</artifactId>
      <version>2.0.1.Final</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.logmanager</groupId>
      <artifactId>jboss-logmanager</artifactId>
      <version>3.0.2.Final</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.logmanager</groupId>
      <artifactId>jboss-logmanager-embedded</artifactId>
      <version>1.2.0.Final</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <exclusions>
        <exclusion>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

  </dependencies>

  <build>
    <plugins>

      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <configuration>
          <systemPropertyVariables>
            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
          </systemPropertyVariables>
        </configuration>
      </plugin>

    </plugins>
  </build>

</project>

If you are using Gradle:

configurations {
    all*.exclude(group: "ch.qos.logback", module: "logback-classic")
}

dependencies {
  implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:6.6.0")
  implementation("org.jboss.logmanager:jboss-logmanager:3.0.2.Final")
  implementation("org.jboss.logmanager:jboss-logmanager-embedded:1.2.0.Final")
  implementation("org.jboss.slf4j:slf4j-jboss-logmanager:2.0.1.Final")
}

tasks.named("bootRun") {
  systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
}

Create a text file called org.jboss.logmanager.ConfiguratorFactory in your resources/META-INF/services/ directory and put the fully qualified name of this library’s DefaultConfiguratorFactory into it:

eu.mulk.quarkus.googlecloud.jsonlogging.logmanager.DefaultConfiguratorFactory

Because Spring Boot configures the system logger with a minimum log level of SEVERE by default, you may also want to configure the logger using a logging.properties file. To do so, first add an entry to application.properties that points to the file:

logging.config = classpath:logging.properties

Create the logging.properties file in your resources directory and set the root logger level to something other than SEVERE:

logger.level = INFO

Finally, add a static block to your Spring Boot application class that disables the Tomcat URL stream handler factory, which conflicts with the URL stream handler factory registered by the JBoss Modules library:

@SpringBootApplication
public class Application {

  static {
    TomcatURLStreamHandlerFactory.disable();
  }

  // ...
}

4. Usage

Logging unstructured data requires no code changes. All logs are automatically converted to Google-Cloud-Logging-compatible JSON.

Structured data can be logged in one of 3 different ways: by passing Labels and StructuredParameters as parameters to individual log entries, by supplying LabelProviders and StructuredParameterProviders, or by using the Mapped Diagnostic Context.

4.1. Using Label and StructuredParameter

Instances of Label and StructuredParameter can be passed as log parameters to the *f family of logging functions on JBoss Logging’s Logger.

Simple key–value pairs are represented by KeyValueParameter.

Example:

logger.logf(
  "Request rejected: unauthorized.",
  Label.of("requestId", "123"),
  KeyValueParameter.of("resource", "/users/mulk"),
  KeyValueParameter.of("method", "PATCH"),
  KeyValueParameter.of("reason", "invalid token"));

Result:

{
  "jsonPayload": {
    "message": "Request rejected: unauthorized.",
    "resource": "/users/mulk",
    "method": "PATCH",
    "reason": "invalid token"
  },
  "labels": {
    "requestId": "123"
  }
}

4.2. Using LabelProvider and StructuredParameterProvider

Any CDI beans that implement LabelProvider and StructuredParameterProvider are discovered at build time and consulted to provide labels and parameters for each message that is logged. This can be used to provide contextual information such as tracing and request IDs stored in thread-local storage.

Alternatively, you can also register providers through the Java ServiceLoader mechanism.

Example:

@Singleton
@Unremovable
public final class TraceLogParameterProvider implements StructuredParameterProvider, LabelProvider {

  @Override
  public StructuredParameter getParameter() {
    var b = Json.createObjectBuilder();
    b.add("traceId", Span.current().getSpanContext().getTraceId());
    b.add("spanId", Span.current().getSpanContext().getSpanId());
    return () -> b;
  }

  @Override
  public Collection<Label> getLabels() {
    return List.of(Label.of("requestId", "123"));
  }
}

Result:

{
  "jsonPayload": {
    "message": "Request rejected: unauthorized.",
    "traceId": "39f9a49a9567a8bd7087b708f8932550",
    "spanId": "c7431b14630b633d"
  },
  "labels": {
    "requestId": "123"
  }
}

4.3. Using the Mapped Diagnostic Context

Any key–value pairs in JBoss Logging’s thread-local MDC are added to the resulting JSON.

Example:

MDC.put("resource", "/users/mulk");
MDC.put("method", "PATCH");
logger.logf("Request rejected: unauthorized.");

Result:

{
  "jsonPayload": {
    "message": "Request rejected: unauthorized.",
    "resource": "/users/mulk",
    "method": "PATCH"
  }
}

5. Development

5.1. Running the Tests

To run the test suite, run:

$ mvn verify

To run the benchmarks, run:

$ mvn verify -Pbenchmark