// SPDX-FileCopyrightText: © 2021 Matthias Andreas Benkard // // SPDX-License-Identifier: LGPL-3.0-or-later package eu.mulk.quarkus.googlecloud.jsonlogging; import io.smallrye.common.constraint.Nullable; import java.time.Instant; import java.util.List; import java.util.Map; import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; /** * A JSON log entry compatible with Google Cloud Logging. * *

Roughly (but not quite) corresponds to Google Cloud Logging's LogEntry * structure. * *

A few of the fields are * treated specially by the fluentd instance running in Google Kubernetes Engine. All other * fields end up in the jsonPayload field on the Google Cloud Logging side. */ final class LogEntry { private final String message; private final String severity; private final Timestamp timestamp; @Nullable private final String trace; @Nullable private final String spanId; private final SourceLocation sourceLocation; private final Map labels; private final List parameters; private final Map mappedDiagnosticContext; @Nullable private final String nestedDiagnosticContext; @Nullable private final String type; LogEntry( String message, String severity, Timestamp timestamp, @Nullable String trace, @Nullable String spanId, SourceLocation sourceLocation, Map labels, List parameters, Map mappedDiagnosticContext, @Nullable String nestedDiagnosticContext, @Nullable String type) { this.message = message; this.severity = severity; this.timestamp = timestamp; this.trace = trace; this.spanId = spanId; this.sourceLocation = sourceLocation; this.labels = labels; this.parameters = parameters; this.mappedDiagnosticContext = mappedDiagnosticContext; this.nestedDiagnosticContext = nestedDiagnosticContext; this.type = type; } static final class SourceLocation { @Nullable private final String file; @Nullable private final String line; @Nullable private final String function; SourceLocation(@Nullable String file, @Nullable String line, @Nullable String function) { this.file = file; this.line = line; this.function = function; } JsonObject json() { var b = Json.createObjectBuilder(); if (file != null) { b.add("file", file); } if (line != null) { b.add("line", line); } if (function != null) { b.add("function", function); } return b.build(); } } static final class Timestamp { private final long seconds; private final int nanos; Timestamp(long seconds, int nanos) { this.seconds = seconds; this.nanos = nanos; } Timestamp(Instant t) { this(t.getEpochSecond(), t.getNano()); } JsonObject json() { return Json.createObjectBuilder().add("seconds", seconds).add("nanos", nanos).build(); } } JsonObjectBuilder json() { var b = Json.createObjectBuilder(); if (trace != null) { b.add("logging.googleapis.com/trace", trace); } if (spanId != null) { b.add("logging.googleapis.com/spanId", spanId); } if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) { b.add("nestedDiagnosticContext", nestedDiagnosticContext); } if (!labels.isEmpty()) { b.add("logging.googleapis.com/labels", jsonOfStringMap(labels)); } if (type != null) { b.add("@type", type); } return b.add("message", message) .add("severity", severity) .add("timestamp", timestamp.json()) .add("logging.googleapis.com/sourceLocation", sourceLocation.json()) .addAll(jsonOfStringMap(mappedDiagnosticContext)) .addAll(jsonOfParameterMap(parameters)); } private static JsonObjectBuilder jsonOfStringMap(Map stringMap) { return stringMap.entrySet().stream() .reduce( Json.createObjectBuilder(), (acc, x) -> acc.add(x.getKey(), x.getValue()), JsonObjectBuilder::addAll); } private static JsonObjectBuilder jsonOfParameterMap(List parameters) { return parameters.stream() .reduce( Json.createObjectBuilder(), (acc, p) -> acc.addAll(p.json()), JsonObjectBuilder::addAll); } }