blob: d108c8198413da1f59fc63635583763a882eab47 [file] [log] [blame]
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02001package eu.mulk.quarkus.googlecloud.jsonlogging;
2
3import io.smallrye.common.constraint.Nullable;
4import java.time.Instant;
5import java.util.List;
6import java.util.Map;
7import javax.json.Json;
8import javax.json.JsonObject;
9import javax.json.JsonObjectBuilder;
10
11/**
12 * A JSON log entry compatible with Google Cloud Logging.
13 *
14 * <p>Roughly (but not quite) corresponds to Google Cloud Logging's <a
15 * href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">LogEntry</a>
16 * structure.
17 *
Matthias Andreas Benkard596486a2021-08-30 12:14:43 +020018 * <p>A few of the fields are <a href="https://cloud.google.com/logging/docs/structured-logging">
19 * treated specially</a> by the fluentd instance running in Google Kubernetes Engine. All other
20 * fields end up in the jsonPayload field on the Google Cloud Logging side.
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020021 */
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +020022final class LogEntry {
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020023
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +020024 private final String message;
25 private final String severity;
26 private final Timestamp timestamp;
27 @Nullable private final String trace;
28 @Nullable private final String spanId;
29 private final SourceLocation sourceLocation;
30 private final Map<String, String> labels;
31 private final List<StructuredParameter> parameters;
32 private final Map<String, String> mappedDiagnosticContext;
33 @Nullable private final String nestedDiagnosticContext;
34 @Nullable private final String type;
35
36 LogEntry(
37 String message,
38 String severity,
39 Timestamp timestamp,
40 @Nullable String trace,
41 @Nullable String spanId,
42 SourceLocation sourceLocation,
43 Map<String, String> labels,
44 List<StructuredParameter> parameters,
45 Map<String, String> mappedDiagnosticContext,
46 @Nullable String nestedDiagnosticContext,
47 @Nullable String type) {
48 this.message = message;
49 this.severity = severity;
50 this.timestamp = timestamp;
51 this.trace = trace;
52 this.spanId = spanId;
53 this.sourceLocation = sourceLocation;
54 this.labels = labels;
55 this.parameters = parameters;
56 this.mappedDiagnosticContext = mappedDiagnosticContext;
57 this.nestedDiagnosticContext = nestedDiagnosticContext;
58 this.type = type;
59 }
60
61 static final class SourceLocation {
62
63 @Nullable private final String file;
64 @Nullable private final String line;
65 @Nullable private final String function;
66
67 SourceLocation(@Nullable String file, @Nullable String line, @Nullable String function) {
68 this.file = file;
69 this.line = line;
70 this.function = function;
71 }
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020072
73 JsonObject json() {
Matthias Andreas Benkarda78c9572021-05-18 21:43:36 +020074 var b = Json.createObjectBuilder();
75
76 if (file != null) {
77 b.add("file", file);
78 }
79
80 if (line != null) {
81 b.add("line", line);
82 }
83
84 if (function != null) {
85 b.add("function", function);
86 }
87
88 return b.build();
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020089 }
90 }
91
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +020092 static final class Timestamp {
93
94 private final long seconds;
95 private final int nanos;
96
97 Timestamp(long seconds, int nanos) {
98 this.seconds = seconds;
99 this.nanos = nanos;
100 }
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200101
102 Timestamp(Instant t) {
103 this(t.getEpochSecond(), t.getNano());
104 }
105
106 JsonObject json() {
107 return Json.createObjectBuilder().add("seconds", seconds).add("nanos", nanos).build();
108 }
109 }
110
111 JsonObjectBuilder json() {
112 var b = Json.createObjectBuilder();
113
114 if (trace != null) {
Matthias Andreas Benkard596486a2021-08-30 12:14:43 +0200115 b.add("logging.googleapis.com/trace", trace);
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200116 }
117
118 if (spanId != null) {
Matthias Andreas Benkard596486a2021-08-30 12:14:43 +0200119 b.add("logging.googleapis.com/spanId", spanId);
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200120 }
121
122 if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) {
123 b.add("nestedDiagnosticContext", nestedDiagnosticContext);
124 }
125
126 if (!labels.isEmpty()) {
Matthias Andreas Benkard596486a2021-08-30 12:14:43 +0200127 b.add("logging.googleapis.com/labels", jsonOfStringMap(labels));
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200128 }
129
130 if (type != null) {
131 b.add("@type", type);
132 }
133
134 return b.add("message", message)
135 .add("severity", severity)
136 .add("timestamp", timestamp.json())
Matthias Andreas Benkard596486a2021-08-30 12:14:43 +0200137 .add("logging.googleapis.com/sourceLocation", sourceLocation.json())
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200138 .addAll(jsonOfStringMap(mappedDiagnosticContext))
139 .addAll(jsonOfParameterMap(parameters));
140 }
141
142 private static JsonObjectBuilder jsonOfStringMap(Map<String, String> stringMap) {
143 return stringMap.entrySet().stream()
144 .reduce(
145 Json.createObjectBuilder(),
146 (acc, x) -> acc.add(x.getKey(), x.getValue()),
147 JsonObjectBuilder::addAll);
148 }
149
150 private static JsonObjectBuilder jsonOfParameterMap(List<StructuredParameter> parameters) {
151 return parameters.stream()
152 .reduce(
153 Json.createObjectBuilder(),
154 (acc, p) -> acc.addAll(p.json()),
155 JsonObjectBuilder::addAll);
156 }
157}