Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable auto-population of missing metadata in logs and printing structured logs to stdout #808

Merged
merged 13 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions google-cloud-logging/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- see http://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
<differences>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/logging/Logging</className>
<method>java.lang.Iterable populateMetadata(java.lang.Iterable, com.google.cloud.MonitoredResource, java.lang.String[])</method>
</difference>
</differences>
5 changes: 5 additions & 0 deletions google-cloud-logging/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static final class Builder {

/** Sets the HTTP request. */
public Builder setRequest(HttpRequest request) {
this.requestBuilder = request.toBuilder();
this.requestBuilder = request != null ? request.toBuilder() : HttpRequest.newBuilder();
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.cloud.MonitoredResource;
import com.google.cloud.logging.Payload.Type;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.logging.v2.LogEntryOperation;
import com.google.logging.v2.LogEntrySourceLocation;
import com.google.logging.v2.LogName;
Expand Down Expand Up @@ -61,8 +69,8 @@ public LogEntry apply(com.google.logging.v2.LogEntry pb) {
private final HttpRequest httpRequest;
private final Map<String, String> labels;
private final Operation operation;
private final Object trace;
private final Object spanId;
private final String trace;
private final String spanId;
private final boolean traceSampled;
private final SourceLocation sourceLocation;
private final Payload<?> payload;
Expand All @@ -80,8 +88,8 @@ public static class Builder {
private HttpRequest httpRequest;
private Map<String, String> labels = new HashMap<>();
private Operation operation;
private Object trace;
private Object spanId;
private String trace;
private String spanId;
private boolean traceSampled;
private SourceLocation sourceLocation;
private Payload<?> payload;
Expand Down Expand Up @@ -245,7 +253,7 @@ public Builder setTrace(String trace) {
* relative resource name, the name is assumed to be relative to `//tracing.googleapis.com`.
*/
public Builder setTrace(Object trace) {
this.trace = trace;
this.trace = trace != null ? trace.toString() : null;
return this;
}

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

/** Sets the ID of the trace span associated with the log entry, if any. */
public Builder setSpanId(Object spanId) {
this.spanId = spanId;
this.spanId = spanId != null ? spanId.toString() : null;
return this;
}

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

/**
* Customized serializers to match the expected format for timestamp, source location and request
* method
*/
static final class InstantSerializer implements JsonSerializer<Instant> {
@Override
public JsonElement serialize(
Instant src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}
}

static final class SourceLocationSerializer implements JsonSerializer<SourceLocation> {
@Override
public JsonElement serialize(
SourceLocation src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
if (src.getFile() != null) {
obj.addProperty("file", src.getFile());
}
if (src.getLine() != null) {
obj.addProperty("line", src.getLine().toString());
}
if (src.getFunction() != null) {
obj.addProperty("function", src.getFunction());
}
return obj;
}
}

static final class RequestMethodSerializer implements JsonSerializer<HttpRequest.RequestMethod> {
@Override
public JsonElement serialize(
HttpRequest.RequestMethod src,
java.lang.reflect.Type typeOfSrc,
JsonSerializationContext context) {
return new JsonPrimitive(src.name());
}
}

/** Helper class to format one line Json representation of the LogEntry for structured log. */
static final class StructuredLogFormatter {
private final Gson gson;
private final StringBuilder builder;

public StructuredLogFormatter(StringBuilder builder) {
checkNotNull(builder);
this.gson =
new GsonBuilder()
.registerTypeAdapter(Instant.class, new InstantSerializer())
.registerTypeAdapter(SourceLocation.class, new SourceLocationSerializer())
.registerTypeAdapter(HttpRequest.RequestMethod.class, new RequestMethodSerializer())
.create();
this.builder = builder;
}

/**
* Adds a Json field and value pair to the current string representation. Method does not
* validate parameters to be multi-line strings. Nothing is added if {@code value} parameter is
* {@code null}.
*
* @param name a valid Json field name string.
* @param value an object to be serialized to Json using {@link Gson}.
* @param appendComma a flag to add a trailing comma.
* @return a reference to this object.
*/
public StructuredLogFormatter appendField(String name, Object value, boolean appendComma) {
checkNotNull(name);
if (value != null) {
builder.append(gson.toJson(name)).append(":").append(gson.toJson(value));
if (!appendComma) {
minherz marked this conversation as resolved.
Show resolved Hide resolved
builder.append(",");
}
}
return this;
}

public StructuredLogFormatter appendField(String name, Object value) {
return appendField(name, value, false);
}

/**
* Serializes a dictionary of key, values as Json fields.
*
* @param value a {@link Map} of key, value arguments to be serialized using {@link Gson}.
* @param appendComma a flag to add a trailing comma.
* @return a reference to this object.
*/
public StructuredLogFormatter appendDict(Map<String, Object> value, boolean appendComma) {
if (value != null) {
String json = gson.toJson(value);
// append json object without brackets
if (json.length() > 1) {
builder.append(json.substring(0, json.length() - 1).substring(1));
if (!appendComma) {
minherz marked this conversation as resolved.
Show resolved Hide resolved
builder.append(",");
}
}
}
return this;
}
}

/**
* Serializes the object to a one line JSON string in the simplified format that can be parsed by
* the logging agents that run on Google Cloud resources.
*/
public String toStructuredJsonString() {
if (payload.getType() == Type.PROTO) {
throw new UnsupportedOperationException("LogEntry with protobuf payload cannot be converted");
}

final StringBuilder builder = new StringBuilder("{");
final StructuredLogFormatter formatter = new StructuredLogFormatter(builder);

formatter
.appendField("severity", severity)
.appendField("timestamp", timestamp)
.appendField("httpRequest", httpRequest)
.appendField("logging.googleapis.com/insertId", insertId)
.appendField("logging.googleapis.com/labels", labels)
.appendField("logging.googleapis.com/operation", operation)
.appendField("logging.googleapis.com/sourceLocation", sourceLocation)
.appendField("logging.googleapis.com/spanId", spanId)
.appendField("logging.googleapis.com/trace", trace)
.appendField("logging.googleapis.com/trace_sampled", traceSampled);
if (payload.getType() == Type.STRING) {
formatter.appendField("message", payload.getData(), true);
} else if (payload.getType() == Type.JSON) {
Payload.JsonPayload jsonPayload = (Payload.JsonPayload) payload;
formatter.appendDict(jsonPayload.getDataAsMap(), true);
}
builder.append("}");
return builder.toString();
}

/** Returns a builder for {@code LogEntry} objects given the entry payload. */
public static Builder newBuilder(Payload<?> payload) {
return new Builder(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ enum OptionType implements Option.OptionType {
LOG_NAME,
RESOURCE,
LABELS,
LOG_DESTINATION;
LOG_DESTINATION,
AUTO_POPULATE_METADATA;

@SuppressWarnings("unchecked")
<T> T get(Map<Option.OptionType, ?> options) {
Expand Down Expand Up @@ -114,6 +115,14 @@ public static WriteOption labels(Map<String, String> labels) {
public static WriteOption destination(LogDestinationName destination) {
return new WriteOption(OptionType.LOG_DESTINATION, destination);
}

/**
* Returns an option to opt-out automatic population of log entries metadata fields that are not
* set.
*/
public static WriteOption autoPopulateMetadata(boolean autoPopulateMetadata) {
return new WriteOption(OptionType.AUTO_POPULATE_METADATA, autoPopulateMetadata);
}
}

/** Fields according to which log entries can be sorted. */
Expand Down Expand Up @@ -1277,8 +1286,30 @@ ApiFuture<AsyncPage<MonitoredResourceDescriptor>> listMonitoredResourceDescripto
* </pre>
*/
@BetaApi("The surface for the tail streaming is not stable yet and may change in the future.")
default LogEntryServerStream tailLogEntries(TailOption... options) {
LogEntryServerStream tailLogEntries(TailOption... options);

/**
* Populates metadata fields of the immutable collection of {@link LogEntry} items. Only empty
* fields are populated. The {@link SourceLocation} is populated only for items with the severity
* set to {@link Severity.DEBUG}. The information about {@link HttpRequest}, trace and span Id is
* retrieved using {@link ContextHandler}.
*
* @param logEntries an immutable collection of {@link LogEntry} items.
* @param customResource a customized instance of the {@link MonitoredResource}. If this parameter
* is {@code null} then the new instance will be generated using {@link
* MonitoredResourceUtil#getResource(String, String)}.
* @param exclusionClassPaths a list of exclussion class path prefixes. If left empty then {@link
* SourceLocation} instance is built based on the caller's stack trace information. Otherwise,
* the information from the first {@link StackTraceElement} along the call stack which class
* name does not start with any not {@code null} exclusion class paths is used.
* @return A collection of {@link LogEntry} items composed from the {@code logEntries} parameter
* with populated metadata fields.
*/
default Iterable<LogEntry> populateMetadata(
Iterable<LogEntry> logEntries,
MonitoredResource customResource,
String... exclusionClassPaths) {
throw new UnsupportedOperationException(
"method tailLogEntriesCallable() does not have default implementation");
"method populateMetadata() does not have default implementation");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class LoggingConfig {
private static final String RESOURCE_TYPE_TAG = "resourceType";
private static final String ENHANCERS_TAG = "enhancers";
private static final String USE_INHERITED_CONTEXT = "useInheritedContext";
private static final String AUTO_POPULATE_METADATA = "autoPopulateMetadata";
private static final String REDIRECT_TO_STDOUT = "redirectToStdout";

public LoggingConfig(String className) {
this.className = className;
Expand Down Expand Up @@ -76,6 +78,14 @@ Formatter getFormatter() {
return getFormatterProperty(FORMATTER_TAG, new SimpleFormatter());
}

Boolean getAutoPopulateMetadata() {
return getBooleanProperty(AUTO_POPULATE_METADATA, null);
}

Boolean getRedirectToStdout() {
return getBooleanProperty(REDIRECT_TO_STDOUT, null);
}

MonitoredResource getMonitoredResource(String projectId) {
String resourceType = getProperty(RESOURCE_TYPE_TAG, "");
return MonitoredResourceUtil.getResource(projectId, resourceType);
Expand All @@ -88,10 +98,11 @@ List<LoggingEnhancer> getEnhancers() {
if (list != null) {
String[] items = list.split(",");
for (String e_name : items) {
Class<? extends LoggingEnhancer> clz =
(Class<? extends LoggingEnhancer>)
ClassLoader.getSystemClassLoader().loadClass(e_name);
enhancers.add(clz.getDeclaredConstructor().newInstance());
Class<? extends LoggingEnhancer> clazz =
ClassLoader.getSystemClassLoader()
.loadClass(e_name)
.asSubclass(LoggingEnhancer.class);
enhancers.add(clazz.getDeclaredConstructor().newInstance());
}
}
return enhancers;
Expand All @@ -117,6 +128,14 @@ private String getProperty(String name, String defaultValue) {
return firstNonNull(getProperty(name), defaultValue);
}

private Boolean getBooleanProperty(String name, Boolean defaultValue) {
String flag = getProperty(name);
if (flag != null) {
return Boolean.parseBoolean(flag);
}
return defaultValue;
}

private Level getLevelProperty(String name, Level defaultValue) {
String stringLevel = getProperty(name);
if (stringLevel == null) {
Expand Down
Loading