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
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.
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);
}