Skip to content

Commit bb25d5d

Browse files
feat: enable auto-population of missing metadata in logs and opting logs redirection to stdout in JUL handler (#808)
Aggregates the following work: - #821 - #812 - #807 - #803 - #798 Fixes #689, #691, #799 and #800 * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 09f1c0c commit bb25d5d

21 files changed

+952
-220
lines changed

.readme-partials.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,62 @@ custom_content: |
134134
```
135135
com.google.cloud.examples.logging.snippets.AddLoggingHandler.handlers=com.google.cloud.logging.LoggingHandler
136136
```
137+
138+
#### Alternative way to ingest logs in Google Cloud managed environments
139+
140+
If you use Java logger with the Cloud Logging Handler, you can configure the handler to output logs to `stdout` using
141+
the [structured logging Json format](https://cloud.google.com/logging/docs/structured-logging#special-payload-fields).
142+
To do this, add `com.google.cloud.logging.LoggingHandler.redirectToStdout=true` to the logger configuration file.
143+
You can use this configuration when running applications in Google Cloud managed environments such as AppEngine, Cloud Run,
144+
Cloud Function or GKE. The logger agent installed on these environments can capture STDOUT and ingest it into Cloud Logging.
145+
The agent can parse structured logs printed to STDOUT and capture additional log metadata beside the log payload.
146+
The parsed information includes severity, source location, user labels, http request and tracing information.
147+
148+
#### Auto-population of log entrys' metadata
149+
150+
LogEntry object metadata information such as [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource),
151+
[Http request](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest) or
152+
[source location](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation)
153+
are automatically populated with information that the library retrieves from the execution context.
154+
The library populates only empty (set to `null`) LogEntry fields.
155+
This behavior in the `Logging` instance can be opted out via `LoggingOptions`.
156+
Call `LoggingOptions.Builder.setAutoPopulateMetadata(false)` to configure logging options to opt-out the metadata auto-population.
157+
Cloud Logging handler can be configured to opt-out automatic population of the metadata using the logger configuration.
158+
To disable the metadata auto-population add `com.google.cloud.logging.LoggingHandler.autoPopulateMetadata=false`
159+
to the logger configuration file.
160+
161+
The auto-population logic populates source location _only_ for log entries with `Severity.DEBUG` severity.
162+
The execution context of the Http request and tracing information is maintained by `ContextHandler` class.
163+
The context is managed in the scope of the thread.
164+
If you do not use thread pools for multi-threading the `ContextHandler` can be configured to propagate the context
165+
to the scope of the child threads.
166+
To enable this add `com.google.cloud.logging.ContextHandler.useInheritedContext=true` to the logger configuration file.
167+
The library provides two methods to update the context:
168+
169+
* Manually set the context. You can use the following methods of the `Context.Builder` to set the context information.
170+
Use the method `setRequest()` to setup the `HttpRequest` instance or `setRequestUrl()`, `setRequestMethod()`,
171+
`setReferer() `, `setRemoteIp()` and `setServerIp()` to setup the fields of the `HttpRequest`.
172+
The trace and span Ids can be set directly using `setTraceId()` and `setSpanId()` respectively.
173+
Alternatively it can be parsed from the W3C tracing context header using `loadW3CTraceParentContext()` or
174+
from the Google Cloud tracing context header using `loadCloudTraceContext()`.
175+
176+
```java
177+
Context context = Context.newBuilder().setHttpRequest(request).setTrace(traceId).setSpanId(spanId).build();
178+
(new ContextHandler()).setCurrentContext(context);
179+
```
180+
181+
* Using [servlet initializer](https://github.com/googleapis/java-logging-servlet-initializer).
182+
If your application uses a Web server based on Jakarta servlets (e.g. Jetty or Tomcat), you can add the servlet initializer
183+
package to your WAR. The package implements a service provider interface (SPI) for
184+
[javax.servlet.ServletContainerInitializer](https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html)
185+
and filters all servlet requests to automatically capture the execution context of the servlet request and store it using
186+
`ContextHandler` class. The stored `Context` class instances are used to populate Http request and tracing information.
187+
If you use Maven, to use the servlet initializer add the following dependency to your BOM:
188+
189+
```xml
190+
<dependency>
191+
<groupId>com.google.cloud</groupId>
192+
<artifactId>google-cloud-logging-servlet-initializer</artifactId>
193+
</dependency>
194+
```
195+

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,64 @@ file. Adding, for instance, the following line:
235235
com.google.cloud.examples.logging.snippets.AddLoggingHandler.handlers=com.google.cloud.logging.LoggingHandler
236236
```
237237

238+
#### Alternative way to ingest logs in Google Cloud managed environments
239+
240+
If you use Java logger with the Cloud Logging Handler, you can configure the handler to output logs to `stdout` using
241+
the [structured logging Json format](https://cloud.google.com/logging/docs/structured-logging#special-payload-fields).
242+
To do this, add `com.google.cloud.logging.LoggingHandler.redirectToStdout=true` to the logger configuration file.
243+
You can use this configuration when running applications in Google Cloud managed environments such as AppEngine, Cloud Run,
244+
Cloud Function or GKE. The logger agent installed on these environments can capture STDOUT and ingest it into Cloud Logging.
245+
The agent can parse structured logs printed to STDOUT and capture additional log metadata beside the log payload.
246+
The parsed information includes severity, source location, user labels, http request and tracing information.
247+
248+
#### Auto-population of log entrys' metadata
249+
250+
LogEntry object metadata information such as [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource),
251+
[Http request](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest) or
252+
[source location](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation)
253+
are automatically populated with information that the library retrieves from the execution context.
254+
The library populates only empty (set to `null`) LogEntry fields.
255+
This behavior in the `Logging` instance can be opted out via `LoggingOptions`.
256+
Call `LoggingOptions.Builder.setAutoPopulateMetadata(false)` to configure logging options to opt-out the metadata auto-population.
257+
Cloud Logging handler can be configured to opt-out automatic population of the metadata using the logger configuration.
258+
To disable the metadata auto-population add `com.google.cloud.logging.LoggingHandler.autoPopulateMetadata=false`
259+
to the logger configuration file.
260+
261+
The auto-population logic populates source location _only_ for log entries with `Severity.DEBUG` severity.
262+
The execution context of the Http request and tracing information is maintained by `ContextHandler` class.
263+
The context is managed in the scope of the thread.
264+
If you do not use thread pools for multi-threading the `ContextHandler` can be configured to propagate the context
265+
to the scope of the child threads.
266+
To enable this add `com.google.cloud.logging.ContextHandler.useInheritedContext=true` to the logger configuration file.
267+
The library provides two methods to update the context:
268+
269+
* Manually set the context. You can use the following methods of the `Context.Builder` to set the context information.
270+
Use the method `setRequest()` to setup the `HttpRequest` instance or `setRequestUrl()`, `setRequestMethod()`,
271+
`setReferer() `, `setRemoteIp()` and `setServerIp()` to setup the fields of the `HttpRequest`.
272+
The trace and span Ids can be set directly using `setTraceId()` and `setSpanId()` respectively.
273+
Alternatively it can be parsed from the W3C tracing context header using `loadW3CTraceParentContext()` or
274+
from the Google Cloud tracing context header using `loadCloudTraceContext()`.
275+
276+
```java
277+
Context context = Context.newBuilder().setHttpRequest(request).setTrace(traceId).setSpanId(spanId).build();
278+
(new ContextHandler()).setCurrentContext(context);
279+
```
280+
281+
* Using [servlet initializer](https://github.com/googleapis/java-logging-servlet-initializer).
282+
If your application uses a Web server based on Jakarta servlets (e.g. Jetty or Tomcat), you can add the servlet initializer
283+
package to your WAR. The package implements a service provider interface (SPI) for
284+
[javax.servlet.ServletContainerInitializer](https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html)
285+
and filters all servlet requests to automatically capture the execution context of the servlet request and store it using
286+
`ContextHandler` class. The stored `Context` class instances are used to populate Http request and tracing information.
287+
If you use Maven, to use the servlet initializer add the following dependency to your BOM:
288+
289+
```xml
290+
<dependency>
291+
<groupId>com.google.cloud</groupId>
292+
<artifactId>google-cloud-logging-servlet-initializer</artifactId>
293+
</dependency>
294+
```
295+
238296

239297

240298

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/Context.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static final class Builder {
4949

5050
/** Sets the HTTP request. */
5151
public Builder setRequest(HttpRequest request) {
52-
this.requestBuilder = request.toBuilder();
52+
this.requestBuilder = request != null ? request.toBuilder() : HttpRequest.newBuilder();
5353
return this;
5454
}
5555

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, true);
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(), false);
714+
} else if (payload.getType() == Type.JSON) {
715+
Payload.JsonPayload jsonPayload = (Payload.JsonPayload) payload;
716+
formatter.appendDict(jsonPayload.getDataAsMap(), false);
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);

0 commit comments

Comments
 (0)