0

Here is the Golang version of what I want to do in Java:

package main

import (
    "log/slog"
    "os"
)

func main() {
    logLevel := new(slog.LevelVar) // Info by default
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}))
    slog.SetDefault(logger)

    slog.Info("this is a test info message",
        slog.Bool("my-flag", true),
        slog.Int("my-int", 123),
        slog.Float64("my-float64", 3.14159),
        slog.String("my-name", "abcdefg"))
}

That generates:

{"time":"2024-04-05T16:54:14.028207-05:00","level":"INFO","msg":"this is a test info message","my-flag":true,"my-int":123,"my-float64":3.14159,"my-name":"abcdefg"}

In Java, I'm aware of three popular logging APIs:

  • System.Logger
  • slf4j
  • the log4j api org.apache.logging.log4j.Logger.

AFAIK, none of these three support named fields like Golang slog does. Is that correct? Am I missing something? It seems like this should be easier.

I tried writing examples in all three logging apis.

3

1 Answer 1

1

If you use logback, for instance, you can use ch.qos.logback.classic.encoder.JsonEncoder to log JSON objects:

You need this configuration in logback.xml:

configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.JsonEncoder"/>
    </appender>
    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

And then the code:

package com.example.so;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Eg {
    public static void main(String[] args) {
        Logger l = LoggerFactory.getLogger("test");

        l.atInfo().addKeyValue("my-flag", true)
                .addKeyValue("my-int", 123)
                .addKeyValue("my-double", Math.PI)
                .addKeyValue("my-name", "abcdefg")
                .log();
    }
}

produces:

{
  "sequenceNumber": 0,
  "timestamp": 1712366291199,
  "nanoseconds": 199243000,
  "level": "INFO",
  "threadName": "main",
  "loggerName": "test",
  "context": {
    "name": "default",
    "birthdate": 1712366291102,
    "properties": {}
  },
  "mdc": {},
  "kvpList": [
    {
      "my-flag": "true"
    },
    {
      "my-int": "123"
    },
    {
      "my-double": "3.141592653589793"
    },
    {
      "my-name": "abcdefg"
    }
  ],
  "message": "null",
  "throwable": null
}

I'm getting the required dependencies via Spring Boot:

[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.2.0-RC1:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:3.2.0-RC1:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:3.2.0-RC1:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.4.11:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.4.11:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.21.0:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.21.0:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:2.0.9:compile

This solution logs all values as JSON strings, even if there's a primitive representation available. The class ch.qos.logback.classic.encoder.JsonEncoder doesn't lend itself to extension, but you can copy it and modify it to log numbers and booleans naturally. (The code below works but has not been tested much)

Add two methods:

    private void appenderMemberUnquotedValue(StringBuilder sb, String key, String value) {
        sb.append(QUOTE).append(key).append(QUOTE_COL).append(value);
    }

    String toJson(Object o) {
        return switch (o) {
            case String s -> "\"" + jsonEscape(s) + "\"";
            case Boolean b -> b.toString();
            case Number n -> n.toString();
            case null -> "null";
            default -> "\"" + jsonEscapedToString(o) + "\"";
        };
    }

And modify one line in this method:

    private void appendKeyValuePairs(StringBuilder sb, ILoggingEvent event) {
        List<KeyValuePair> kvpList = event.getKeyValuePairs();
        if (kvpList == null || kvpList.isEmpty())
            return;

        sb.append(QUOTE).append(KEY_VALUE_PAIRS_ATTR_NAME).append(QUOTE_COL).append(SP).append(OPEN_ARRAY);
        final int len = kvpList.size();
        for (int i = 0; i < len; i++) {
            if (i != 0)
                sb.append(VALUE_SEPARATOR);
            KeyValuePair kvp = kvpList.get(i);
            sb.append(OPEN_OBJ);
//             appenderMember(sb, jsonEscapedToString(kvp.key), jsonEscapedToString(kvp.value)); // OLD

            appenderMemberUnquotedValue(sb, jsonEscapedToString(kvp.key), toJson(kvp.value)); // NEW
            sb.append(CLOSE_OBJ);
        }
        sb.append(CLOSE_ARRAY);
        sb.append(VALUE_SEPARATOR);
    }
Sign up to request clarification or add additional context in comments.

1 Comment

that's great. One flaw is the extra key-values are all logged as JSON strings, and not JSON booleans or JSON numerics.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.