Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit 0c717d3

Browse files
committed
feat: support structured logging in LoggingHandler using configuration (#812)
Implements populateMetadata() API. Changes Logging.write() API to populate the provided list of log entries using the new API. Refactors SourceLocation.fromCurrentContext() to use a list of exclusion instead of the call stack depth level as parameter. Adds configuration `populateToStdout` to `LoggingConfig`. Use the new configuration within `LoggingHandler` to print to STDOUT instead of ingesting the log by calling Logging.write(). Refactor LoggingImpl, LoggingHandler and unit tests.
1 parent 8ea8c59 commit 0c717d3

12 files changed

Lines changed: 465 additions & 211 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- see http://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
3+
<differences>
4+
<difference>
5+
<differenceType>7012</differenceType>
6+
<className>com/google/cloud/logging/Logging</className>
7+
<method>java.lang.Iterable populateMetadata(java.lang.Iterable, com.google.cloud.MonitoredResource, java.lang.String[])</method>
8+
</difference>
9+
</differences>

google-cloud-logging/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<groupId>com.google.guava</groupId>
2222
<artifactId>guava</artifactId>
2323
</dependency>
24+
<dependency>
25+
<groupId>com.google.code.gson</groupId>
26+
<artifactId>gson</artifactId>
27+
<version>2.8.9</version>
28+
</dependency>
2429
<dependency>
2530
<groupId>io.grpc</groupId>
2631
<artifactId>grpc-api</artifactId>

google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020

2121
import com.google.cloud.MonitoredResource;
22+
import com.google.cloud.logging.Payload.Type;
2223
import com.google.common.base.Function;
2324
import com.google.common.base.MoreObjects;
2425
import com.google.common.collect.ImmutableMap;
26+
import com.google.gson.Gson;
27+
import com.google.gson.GsonBuilder;
28+
import com.google.gson.JsonElement;
29+
import com.google.gson.JsonObject;
30+
import com.google.gson.JsonPrimitive;
31+
import com.google.gson.JsonSerializationContext;
32+
import com.google.gson.JsonSerializer;
2533
import com.google.logging.v2.LogEntryOperation;
2634
import com.google.logging.v2.LogEntrySourceLocation;
2735
import com.google.logging.v2.LogName;
@@ -61,8 +69,8 @@ public LogEntry apply(com.google.logging.v2.LogEntry pb) {
6169
private final HttpRequest httpRequest;
6270
private final Map<String, String> labels;
6371
private final Operation operation;
64-
private final Object trace;
65-
private final Object spanId;
72+
private final String trace;
73+
private final String spanId;
6674
private final boolean traceSampled;
6775
private final SourceLocation sourceLocation;
6876
private final Payload<?> payload;
@@ -80,8 +88,8 @@ public static class Builder {
8088
private HttpRequest httpRequest;
8189
private Map<String, String> labels = new HashMap<>();
8290
private Operation operation;
83-
private Object trace;
84-
private Object spanId;
91+
private String trace;
92+
private String spanId;
8593
private boolean traceSampled;
8694
private SourceLocation sourceLocation;
8795
private Payload<?> payload;
@@ -245,7 +253,7 @@ public Builder setTrace(String trace) {
245253
* relative resource name, the name is assumed to be relative to `//tracing.googleapis.com`.
246254
*/
247255
public Builder setTrace(Object trace) {
248-
this.trace = trace;
256+
this.trace = trace != null ? trace.toString() : null;
249257
return this;
250258
}
251259

@@ -257,7 +265,7 @@ public Builder setSpanId(String spanId) {
257265

258266
/** Sets the ID of the trace span associated with the log entry, if any. */
259267
public Builder setSpanId(Object spanId) {
260-
this.spanId = spanId;
268+
this.spanId = spanId != null ? spanId.toString() : null;
261269
return this;
262270
}
263271

@@ -575,6 +583,142 @@ com.google.logging.v2.LogEntry toPb(String projectId) {
575583
return builder.build();
576584
}
577585

586+
/**
587+
* Customized serializers to match the expected format for timestamp, source location and request
588+
* method
589+
*/
590+
static final class InstantSerializer implements JsonSerializer<Instant> {
591+
@Override
592+
public JsonElement serialize(
593+
Instant src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
594+
return new JsonPrimitive(src.toString());
595+
}
596+
}
597+
598+
static final class SourceLocationSerializer implements JsonSerializer<SourceLocation> {
599+
@Override
600+
public JsonElement serialize(
601+
SourceLocation src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
602+
JsonObject obj = new JsonObject();
603+
if (src.getFile() != null) {
604+
obj.addProperty("file", src.getFile());
605+
}
606+
if (src.getLine() != null) {
607+
obj.addProperty("line", src.getLine().toString());
608+
}
609+
if (src.getFunction() != null) {
610+
obj.addProperty("function", src.getFunction());
611+
}
612+
return obj;
613+
}
614+
}
615+
616+
static final class RequestMethodSerializer implements JsonSerializer<HttpRequest.RequestMethod> {
617+
@Override
618+
public JsonElement serialize(
619+
HttpRequest.RequestMethod src,
620+
java.lang.reflect.Type typeOfSrc,
621+
JsonSerializationContext context) {
622+
return new JsonPrimitive(src.name());
623+
}
624+
}
625+
626+
/** Helper class to format one line Json representation of the LogEntry for structured log. */
627+
static final class StructuredLogFormatter {
628+
private final Gson gson;
629+
private final StringBuilder builder;
630+
631+
public StructuredLogFormatter(StringBuilder builder) {
632+
checkNotNull(builder);
633+
this.gson =
634+
new GsonBuilder()
635+
.registerTypeAdapter(Instant.class, new InstantSerializer())
636+
.registerTypeAdapter(SourceLocation.class, new SourceLocationSerializer())
637+
.registerTypeAdapter(HttpRequest.RequestMethod.class, new RequestMethodSerializer())
638+
.create();
639+
this.builder = builder;
640+
}
641+
642+
/**
643+
* Adds a Json field and value pair to the current string representation. Method does not
644+
* validate parameters to be multi-line strings. Nothing is added if {@code value} parameter is
645+
* {@code null}.
646+
*
647+
* @param name a valid Json field name string.
648+
* @param value an object to be serialized to Json using {@link Gson}.
649+
* @param appendComma a flag to add a trailing comma.
650+
* @return a reference to this object.
651+
*/
652+
public StructuredLogFormatter appendField(String name, Object value, boolean appendComma) {
653+
checkNotNull(name);
654+
if (value != null) {
655+
builder.append(gson.toJson(name)).append(":").append(gson.toJson(value));
656+
if (!appendComma) {
657+
builder.append(",");
658+
}
659+
}
660+
return this;
661+
}
662+
663+
public StructuredLogFormatter appendField(String name, Object value) {
664+
return appendField(name, value, false);
665+
}
666+
667+
/**
668+
* Serializes a dictionary of key, values as Json fields.
669+
*
670+
* @param value a {@link Map} of key, value arguments to be serialized using {@link Gson}.
671+
* @param appendComma a flag to add a trailing comma.
672+
* @return a reference to this object.
673+
*/
674+
public StructuredLogFormatter appendDict(Map<String, Object> value, boolean appendComma) {
675+
if (value != null) {
676+
String json = gson.toJson(value);
677+
// append json object without brackets
678+
if (json.length() > 1) {
679+
builder.append(json.substring(0, json.length() - 1).substring(1));
680+
if (!appendComma) {
681+
builder.append(",");
682+
}
683+
}
684+
}
685+
return this;
686+
}
687+
}
688+
689+
/**
690+
* Serializes the object to a one line JSON string in the simplified format that can be parsed by
691+
* the logging agents that run on Google Cloud resources.
692+
*/
693+
public String toStructuredJsonString() {
694+
if (payload.getType() == Type.PROTO) {
695+
throw new UnsupportedOperationException("LogEntry with protobuf payload cannot be converted");
696+
}
697+
698+
final StringBuilder builder = new StringBuilder("{");
699+
final StructuredLogFormatter formatter = new StructuredLogFormatter(builder);
700+
701+
formatter
702+
.appendField("severity", severity)
703+
.appendField("timestamp", timestamp)
704+
.appendField("httpRequest", httpRequest)
705+
.appendField("logging.googleapis.com/insertId", insertId)
706+
.appendField("logging.googleapis.com/labels", labels)
707+
.appendField("logging.googleapis.com/operation", operation)
708+
.appendField("logging.googleapis.com/sourceLocation", sourceLocation)
709+
.appendField("logging.googleapis.com/spanId", spanId)
710+
.appendField("logging.googleapis.com/trace", trace)
711+
.appendField("logging.googleapis.com/trace_sampled", traceSampled);
712+
if (payload.getType() == Type.STRING) {
713+
formatter.appendField("message", payload.getData(), true);
714+
} else if (payload.getType() == Type.JSON) {
715+
Payload.JsonPayload jsonPayload = (Payload.JsonPayload) payload;
716+
formatter.appendDict(jsonPayload.getDataAsMap(), true);
717+
}
718+
builder.append("}");
719+
return builder.toString();
720+
}
721+
578722
/** Returns a builder for {@code LogEntry} objects given the entry payload. */
579723
public static Builder newBuilder(Payload<?> payload) {
580724
return new Builder(payload);

google-cloud-logging/src/main/java/com/google/cloud/logging/Logging.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,8 +1286,30 @@ ApiFuture<AsyncPage<MonitoredResourceDescriptor>> listMonitoredResourceDescripto
12861286
* </pre>
12871287
*/
12881288
@BetaApi("The surface for the tail streaming is not stable yet and may change in the future.")
1289-
default LogEntryServerStream tailLogEntries(TailOption... options) {
1289+
LogEntryServerStream tailLogEntries(TailOption... options);
1290+
1291+
/**
1292+
* Populates metadata fields of the immutable collection of {@link LogEntry} items. Only empty
1293+
* fields are populated. The {@link SourceLocation} is populated only for items with the severity
1294+
* set to {@link Severity.DEBUG}. The information about {@link HttpRequest}, trace and span Id is
1295+
* retrieved using {@link ContextHandler}.
1296+
*
1297+
* @param logEntries an immutable collection of {@link LogEntry} items.
1298+
* @param customResource a customized instance of the {@link MonitoredResource}. If this parameter
1299+
* is {@code null} then the new instance will be generated using {@link
1300+
* MonitoredResourceUtil#getResource(String, String)}.
1301+
* @param exclusionClassPaths a list of exclussion class path prefixes. If left empty then {@link
1302+
* SourceLocation} instance is built based on the caller's stack trace information. Otherwise,
1303+
* the information from the first {@link StackTraceElement} along the call stack which class
1304+
* name does not start with any not {@code null} exclusion class paths is used.
1305+
* @return A collection of {@link LogEntry} items composed from the {@code logEntries} parameter
1306+
* with populated metadata fields.
1307+
*/
1308+
default Iterable<LogEntry> populateMetadata(
1309+
Iterable<LogEntry> logEntries,
1310+
MonitoredResource customResource,
1311+
String... exclusionClassPaths) {
12901312
throw new UnsupportedOperationException(
1291-
"method tailLogEntriesCallable() does not have default implementation");
1313+
"method populateMetadata() does not have default implementation");
12921314
}
12931315
}

google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingConfig.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class LoggingConfig {
4242
private static final String ENHANCERS_TAG = "enhancers";
4343
private static final String USE_INHERITED_CONTEXT = "useInheritedContext";
4444
private static final String AUTO_POPULATE_METADATA = "autoPopulateMetadata";
45+
private static final String REDIRECT_TO_STDOUT = "redirectToStdout";
4546

4647
public LoggingConfig(String className) {
4748
this.className = className;
@@ -78,11 +79,11 @@ Formatter getFormatter() {
7879
}
7980

8081
Boolean getAutoPopulateMetadata() {
81-
String flag = getProperty(AUTO_POPULATE_METADATA);
82-
if (flag != null) {
83-
return Boolean.parseBoolean(flag);
84-
}
85-
return null;
82+
return getBooleanProperty(AUTO_POPULATE_METADATA, null);
83+
}
84+
85+
Boolean getRedirectToStdout() {
86+
return getBooleanProperty(REDIRECT_TO_STDOUT, null);
8687
}
8788

8889
MonitoredResource getMonitoredResource(String projectId) {
@@ -127,6 +128,14 @@ private String getProperty(String name, String defaultValue) {
127128
return firstNonNull(getProperty(name), defaultValue);
128129
}
129130

131+
private Boolean getBooleanProperty(String name, Boolean defaultValue) {
132+
String flag = getProperty(name);
133+
if (flag != null) {
134+
return Boolean.parseBoolean(flag);
135+
}
136+
return defaultValue;
137+
}
138+
130139
private Level getLevelProperty(String name, Level defaultValue) {
131140
String stringLevel = getProperty(name);
132141
if (stringLevel == null) {

0 commit comments

Comments
 (0)