blob: 2d08c292ae7b532fadffcad1a94bb3637b5548c4 [file] [log] [blame]
// SPDX-FileCopyrightText: © 2021 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
//
// SPDX-License-Identifier: LGPL-3.0-or-later
package eu.mulk.quarkus.googlecloud.jsonlogging;
import io.smallrye.common.constraint.Nullable;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* A JSON log entry compatible with Google Cloud Logging.
*
* <p>Roughly (but not quite) corresponds to Google Cloud Logging's <a
* href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">LogEntry</a>
* structure.
*
* <p>A few of the fields are <a href="https://cloud.google.com/logging/docs/structured-logging">
* treated specially</a> 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;
@Nullable private final SourceLocation sourceLocation;
private final Map<String, String> labels;
private final List<StructuredParameter> parameters;
private final Map<String, String> mappedDiagnosticContext;
@Nullable private final String nestedDiagnosticContext;
@Nullable private final String type;
@Nullable private final String insertId;
LogEntry(
String message,
String severity,
Timestamp timestamp,
@Nullable String trace,
@Nullable String spanId,
@Nullable SourceLocation sourceLocation,
Map<String, String> labels,
List<StructuredParameter> parameters,
Map<String, String> mappedDiagnosticContext,
@Nullable String nestedDiagnosticContext,
@Nullable String type,
@Nullable String insertId) {
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;
this.insertId = insertId;
}
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;
}
void json(StringBuilder b) {
var commaNeeded = false;
if (file != null) {
b.append("\"file\":");
appendJsonString(b, file);
commaNeeded = true;
}
if (line != null) {
if (commaNeeded) {
b.append(",");
}
b.append("\"line\":");
appendJsonString(b, line);
commaNeeded = true;
}
if (function != null) {
if (commaNeeded) {
b.append(",");
}
b.append("\"function\":");
appendJsonString(b, function);
}
}
}
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());
}
void json(StringBuilder b) {
b.append("\"seconds\":");
b.append(seconds);
b.append(",\"nanos\":");
b.append(nanos);
}
}
void json(StringBuilder b) {
if (insertId != null) {
b.append("\"logging.googleapis.com/insertId\":");
appendJsonString(b, insertId);
b.append(",");
}
if (trace != null) {
b.append("\"logging.googleapis.com/trace\":");
appendJsonString(b, trace);
b.append(",");
}
if (spanId != null) {
b.append("\"logging.googleapis.com/spanId\":");
appendJsonString(b, spanId);
b.append(",");
}
if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) {
b.append("\"nestedDiagnosticContext\":");
appendJsonString(b, nestedDiagnosticContext);
b.append(",");
}
if (!labels.isEmpty()) {
b.append("\"logging.googleapis.com/labels\":{");
var first = true;
for (var entry : labels.entrySet()) {
if (!first) {
b.append(",");
} else {
first = false;
}
appendJsonString(b, entry.getKey());
b.append(":");
appendJsonString(b, entry.getValue());
}
b.append("},");
}
for (var entry : mappedDiagnosticContext.entrySet()) {
appendJsonString(b, entry.getKey());
b.append(":");
appendJsonString(b, entry.getValue());
b.append(",");
}
for (var parameter : parameters) {
var jsonObject = parameter.json().build();
jsonObject.forEach(
(key, value) -> {
appendJsonString(b, key);
b.append(":");
appendJsonObject(b, value);
b.append(",");
});
}
if (type != null) {
b.append("\"@type\":");
appendJsonString(b, type);
b.append(",");
}
if (sourceLocation != null) {
b.append("\"logging.googleapis.com/sourceLocation\":{");
sourceLocation.json(b);
b.append("},");
}
b.append("\"message\":");
appendJsonString(b, message);
b.append(",\"severity\":");
appendJsonString(b, severity);
b.append(",\"timestamp\":{");
timestamp.json(b);
b.append("}");
}
private static void appendJsonObject(StringBuilder b, JsonValue value) {
switch (value.getValueType()) {
case ARRAY:
b.append("[");
var array = value.asJsonArray();
for (var i = 0; i < array.size(); i++) {
if (i > 0) {
b.append(",");
}
appendJsonObject(b, array.get(i));
}
b.append("]");
break;
case OBJECT:
b.append("{");
var object = value.asJsonObject();
var first = true;
for (var entry : object.entrySet()) {
if (!first) {
b.append(",");
} else {
first = false;
}
appendJsonString(b, entry.getKey());
b.append(":");
appendJsonObject(b, entry.getValue());
}
b.append("}");
break;
case STRING:
appendJsonString(b, ((JsonString) value).getString());
break;
case NUMBER:
b.append(value);
break;
case TRUE:
b.append("true");
break;
case FALSE:
b.append("false");
break;
case NULL:
b.append("null");
break;
}
}
private static void appendJsonString(StringBuilder b, String s) {
b.append('"');
for (var i = 0; i < s.length(); i++) {
var c = s.charAt(i);
switch (c) {
case '"':
b.append("\\\"");
break;
case '\\':
b.append("\\\\");
break;
case '\b':
b.append("\\b");
break;
case '\f':
b.append("\\f");
break;
case '\n':
b.append("\\n");
break;
case '\r':
b.append("\\r");
break;
case '\t':
b.append("\\t");
break;
default:
if (c < 0x20) {
b.append("\\u00");
b.append(Character.forDigit((c >> 4) & 0xf, 16));
b.append(Character.forDigit(c & 0xf, 16));
} else {
b.append(c);
}
}
}
b.append('"');
}
}