diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 95e214c54..557361955 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -134,3 +134,62 @@ custom_content: | ``` com.google.cloud.examples.logging.snippets.AddLoggingHandler.handlers=com.google.cloud.logging.LoggingHandler ``` + + #### Alternative way to ingest logs in Google Cloud managed environments + + If you use Java logger with the Cloud Logging Handler, you can configure the handler to output logs to `stdout` using + the [structured logging Json format](https://cloud.google.com/logging/docs/structured-logging#special-payload-fields). + To do this, add `com.google.cloud.logging.LoggingHandler.redirectToStdout=true` to the logger configuration file. + You can use this configuration when running applications in Google Cloud managed environments such as AppEngine, Cloud Run, + Cloud Function or GKE. The logger agent installed on these environments can capture STDOUT and ingest it into Cloud Logging. + The agent can parse structured logs printed to STDOUT and capture additional log metadata beside the log payload. + The parsed information includes severity, source location, user labels, http request and tracing information. + + #### Auto-population of log entrys' metadata + + LogEntry object metadata information such as [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource), + [Http request](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest) or + [source location](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation) + are automatically populated with information that the library retrieves from the execution context. + The library populates only empty (set to `null`) LogEntry fields. + This behavior in the `Logging` instance can be opted out via `LoggingOptions`. + Call `LoggingOptions.Builder.setAutoPopulateMetadata(false)` to configure logging options to opt-out the metadata auto-population. + Cloud Logging handler can be configured to opt-out automatic population of the metadata using the logger configuration. + To disable the metadata auto-population add `com.google.cloud.logging.LoggingHandler.autoPopulateMetadata=false` + to the logger configuration file. + + The auto-population logic populates source location _only_ for log entries with `Severity.DEBUG` severity. + The execution context of the Http request and tracing information is maintained by `ContextHandler` class. + The context is managed in the scope of the thread. + If you do not use thread pools for multi-threading the `ContextHandler` can be configured to propagate the context + to the scope of the child threads. + To enable this add `com.google.cloud.logging.ContextHandler.useInheritedContext=true` to the logger configuration file. + The library provides two methods to update the context: + + * Manually set the context. You can use the following methods of the `Context.Builder` to set the context information. + Use the method `setRequest()` to setup the `HttpRequest` instance or `setRequestUrl()`, `setRequestMethod()`, + `setReferer() `, `setRemoteIp()` and `setServerIp()` to setup the fields of the `HttpRequest`. + The trace and span Ids can be set directly using `setTraceId()` and `setSpanId()` respectively. + Alternatively it can be parsed from the W3C tracing context header using `loadW3CTraceParentContext()` or + from the Google Cloud tracing context header using `loadCloudTraceContext()`. + + ```java + Context context = Context.newBuilder().setHttpRequest(request).setTrace(traceId).setSpanId(spanId).build(); + (new ContextHandler()).setCurrentContext(context); + ``` + + * Using [servlet initializer](https://github.com/googleapis/java-logging-servlet-initializer). + If your application uses a Web server based on Jakarta servlets (e.g. Jetty or Tomcat), you can add the servlet initializer + package to your WAR. The package implements a service provider interface (SPI) for + [javax.servlet.ServletContainerInitializer](https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html) + and filters all servlet requests to automatically capture the execution context of the servlet request and store it using + `ContextHandler` class. The stored `Context` class instances are used to populate Http request and tracing information. + If you use Maven, to use the servlet initializer add the following dependency to your BOM: + + ```xml + + com.google.cloud + google-cloud-logging-servlet-initializer + + ``` + diff --git a/README.md b/README.md index fd289b746..b6eab609a 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,64 @@ file. Adding, for instance, the following line: com.google.cloud.examples.logging.snippets.AddLoggingHandler.handlers=com.google.cloud.logging.LoggingHandler ``` +#### Alternative way to ingest logs in Google Cloud managed environments + +If you use Java logger with the Cloud Logging Handler, you can configure the handler to output logs to `stdout` using +the [structured logging Json format](https://cloud.google.com/logging/docs/structured-logging#special-payload-fields). +To do this, add `com.google.cloud.logging.LoggingHandler.redirectToStdout=true` to the logger configuration file. +You can use this configuration when running applications in Google Cloud managed environments such as AppEngine, Cloud Run, +Cloud Function or GKE. The logger agent installed on these environments can capture STDOUT and ingest it into Cloud Logging. +The agent can parse structured logs printed to STDOUT and capture additional log metadata beside the log payload. +The parsed information includes severity, source location, user labels, http request and tracing information. + +#### Auto-population of log entrys' metadata + +LogEntry object metadata information such as [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource), +[Http request](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest) or +[source location](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation) +are automatically populated with information that the library retrieves from the execution context. +The library populates only empty (set to `null`) LogEntry fields. +This behavior in the `Logging` instance can be opted out via `LoggingOptions`. +Call `LoggingOptions.Builder.setAutoPopulateMetadata(false)` to configure logging options to opt-out the metadata auto-population. +Cloud Logging handler can be configured to opt-out automatic population of the metadata using the logger configuration. +To disable the metadata auto-population add `com.google.cloud.logging.LoggingHandler.autoPopulateMetadata=false` +to the logger configuration file. + +The auto-population logic populates source location _only_ for log entries with `Severity.DEBUG` severity. +The execution context of the Http request and tracing information is maintained by `ContextHandler` class. +The context is managed in the scope of the thread. +If you do not use thread pools for multi-threading the `ContextHandler` can be configured to propagate the context +to the scope of the child threads. +To enable this add `com.google.cloud.logging.ContextHandler.useInheritedContext=true` to the logger configuration file. +The library provides two methods to update the context: + +* Manually set the context. You can use the following methods of the `Context.Builder` to set the context information. +Use the method `setRequest()` to setup the `HttpRequest` instance or `setRequestUrl()`, `setRequestMethod()`, +`setReferer() `, `setRemoteIp()` and `setServerIp()` to setup the fields of the `HttpRequest`. +The trace and span Ids can be set directly using `setTraceId()` and `setSpanId()` respectively. +Alternatively it can be parsed from the W3C tracing context header using `loadW3CTraceParentContext()` or +from the Google Cloud tracing context header using `loadCloudTraceContext()`. + + ```java + Context context = Context.newBuilder().setHttpRequest(request).setTrace(traceId).setSpanId(spanId).build(); + (new ContextHandler()).setCurrentContext(context); + ``` + +* Using [servlet initializer](https://github.com/googleapis/java-logging-servlet-initializer). +If your application uses a Web server based on Jakarta servlets (e.g. Jetty or Tomcat), you can add the servlet initializer +package to your WAR. The package implements a service provider interface (SPI) for +[javax.servlet.ServletContainerInitializer](https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html) +and filters all servlet requests to automatically capture the execution context of the servlet request and store it using +`ContextHandler` class. The stored `Context` class instances are used to populate Http request and tracing information. +If you use Maven, to use the servlet initializer add the following dependency to your BOM: + + ```xml + + com.google.cloud + google-cloud-logging-servlet-initializer + + ``` + diff --git a/google-cloud-logging/clirr-ignored-differences.xml b/google-cloud-logging/clirr-ignored-differences.xml new file mode 100644 index 000000000..0d780d1a4 --- /dev/null +++ b/google-cloud-logging/clirr-ignored-differences.xml @@ -0,0 +1,9 @@ + + + + + 7012 + com/google/cloud/logging/Logging + java.lang.Iterable populateMetadata(java.lang.Iterable, com.google.cloud.MonitoredResource, java.lang.String[]) + + diff --git a/google-cloud-logging/pom.xml b/google-cloud-logging/pom.xml index 942d8d57b..e819869d9 100644 --- a/google-cloud-logging/pom.xml +++ b/google-cloud-logging/pom.xml @@ -21,6 +21,11 @@ com.google.guava guava + + com.google.code.gson + gson + 2.8.9 + io.grpc grpc-api diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java index 97867cdee..832d4ed2c 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java @@ -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; } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java index 16b469cd8..17c89a7cc 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java @@ -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; @@ -61,8 +69,8 @@ public LogEntry apply(com.google.logging.v2.LogEntry pb) { private final HttpRequest httpRequest; private final Map 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; @@ -80,8 +88,8 @@ public static class Builder { private HttpRequest httpRequest; private Map 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; @@ -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; } @@ -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; } @@ -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 { + @Override + public JsonElement serialize( + Instant src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + } + + static final class SourceLocationSerializer implements JsonSerializer { + @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 { + @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) { + builder.append(","); + } + } + return this; + } + + public StructuredLogFormatter appendField(String name, Object value) { + return appendField(name, value, true); + } + + /** + * 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 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) { + 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(), false); + } else if (payload.getType() == Type.JSON) { + Payload.JsonPayload jsonPayload = (Payload.JsonPayload) payload; + formatter.appendDict(jsonPayload.getDataAsMap(), false); + } + 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); diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/Logging.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/Logging.java index b665d96fd..a765c73e0 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/Logging.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/Logging.java @@ -70,7 +70,8 @@ enum OptionType implements Option.OptionType { LOG_NAME, RESOURCE, LABELS, - LOG_DESTINATION; + LOG_DESTINATION, + AUTO_POPULATE_METADATA; @SuppressWarnings("unchecked") T get(Map options) { @@ -114,6 +115,14 @@ public static WriteOption labels(Map 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. */ @@ -1277,8 +1286,30 @@ ApiFuture> listMonitoredResourceDescripto * */ @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 populateMetadata( + Iterable logEntries, + MonitoredResource customResource, + String... exclusionClassPaths) { throw new UnsupportedOperationException( - "method tailLogEntriesCallable() does not have default implementation"); + "method populateMetadata() does not have default implementation"); } } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingConfig.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingConfig.java index d2a001fb0..923fa6b52 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingConfig.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingConfig.java @@ -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; @@ -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); @@ -88,10 +98,11 @@ List getEnhancers() { if (list != null) { String[] items = list.split(","); for (String e_name : items) { - Class clz = - (Class) - ClassLoader.getSystemClassLoader().loadClass(e_name); - enhancers.add(clz.getDeclaredConstructor().newInstance()); + Class clazz = + ClassLoader.getSystemClassLoader() + .loadClass(e_name) + .asSubclass(LoggingEnhancer.class); + enhancers.add(clazz.getDeclaredConstructor().newInstance()); } } return enhancers; @@ -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) { diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java index db18fe1a1..983127d5a 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java @@ -25,9 +25,11 @@ import com.google.common.collect.Iterables; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.logging.ErrorManager; import java.util.logging.Filter; import java.util.logging.Formatter; @@ -109,6 +111,15 @@ * else "global"). *
  • {@code com.google.cloud.logging.Synchronicity} the synchronicity of the write method to use * to write logs to the Cloud Logging service (defaults to {@link Synchronicity#ASYNC}). + *
  • {@code com.google.cloud.logging.LoggingHandler.autoPopulateMetadata} is a boolean flag that + * opts-out the population of the log entries metadata before the logs are sent to Cloud + * Logging (defaults to {@code true}). + *
  • {@code com.google.cloud.logging.LoggingHandler.redirectToStdout} is a boolean flag that + * opts-in redirecting the output of the handler to STDOUT instead of ingesting logs to Cloud + * Logging using Logging API (defaults to {@code true}). Redirecting logs can be used in + * Google Cloud environments with installed logging agent to delegate log ingestions to the + * agent. Redirected logs are formatted as one line Json string following the structured + * logging guidelines. * * *

    To add a {@code LoggingHandler} to an existing {@link Logger} and be sure to avoid infinite @@ -118,6 +129,8 @@ *

      * {@code com.example.mypackage.handlers=com.google.cloud.logging.LoggingHandler}
      * 
    + * + * @see Structured logging */ public class LoggingHandler extends Handler { @@ -139,6 +152,9 @@ public class LoggingHandler extends Handler { private volatile Level flushLevel; + private volatile Boolean autoPopulateMetadata; + private volatile Boolean redirectToStdout; + private WriteOption[] defaultWriteOptions; /** Creates an handler that publishes messages to Cloud Logging. */ @@ -196,7 +212,10 @@ public LoggingHandler( } /** - * Creates a handler that publishes messages to Cloud Logging. + * Creates a handler that publishes messages to Cloud Logging. Auto-population of the logs + * metadata can be opted-out in {@code options} argument or in the configuration file. At least + * one flag {@link LoggingOptions} or {@link LoggingConfig} has to be explicitly set to {@code + * false} in order to opt-out the metadata auto-population. * * @param log the name of the log to which log entries are written * @param options options for the Cloud Logging service @@ -222,14 +241,19 @@ public LoggingHandler( setLevel(level); baseLevel = level.equals(Level.ALL) ? Level.FINEST : level; flushLevel = config.getFlushLevel(); + Boolean f1 = options.getAutoPopulateMetadata(); + Boolean f2 = config.getAutoPopulateMetadata(); + autoPopulateMetadata = isTrueOrNull(f1) && isTrueOrNull(f2); + redirectToStdout = firstNonNull(config.getRedirectToStdout(), Boolean.FALSE); String logName = log != null ? log : config.getLogName(); - MonitoredResource resource = firstNonNull( monitoredResource, config.getMonitoredResource(loggingOptions.getProjectId())); List writeOptions = new ArrayList(); writeOptions.add(WriteOption.logName(logName)); - writeOptions.add(WriteOption.resource(resource)); + if (resource != null) { + writeOptions.add(WriteOption.resource(resource)); + } writeOptions.add( WriteOption.labels( ImmutableMap.of( @@ -242,8 +266,9 @@ public LoggingHandler( } defaultWriteOptions = Iterables.toArray(writeOptions, WriteOption.class); - getLogging().setFlushSeverity(severityFor(flushLevel)); - getLogging().setWriteSynchronicity(config.getSynchronicity()); + logging = loggingOptions.getService(); + logging.setFlushSeverity(severityFor(flushLevel)); + logging.setWriteSynchronicity(config.getSynchronicity()); this.enhancers = new LinkedList<>(); @@ -287,13 +312,34 @@ public void publish(LogRecord record) { } if (logEntry != null) { try { - getLogging().write(ImmutableList.of(logEntry), defaultWriteOptions); + Iterable logEntries = ImmutableList.of(logEntry); + if (autoPopulateMetadata) { + logEntries = + logging.populateMetadata( + logEntries, getMonitoredResource(), "com.google.cloud.logging", "java"); + } + if (redirectToStdout) { + logEntries.forEach(log -> System.out.println(log.toStructuredJsonString())); + } else { + logging.write(logEntries, defaultWriteOptions); + } } catch (Exception ex) { getErrorManager().error(null, ex, ErrorManager.WRITE_FAILURE); } } } + private MonitoredResource getMonitoredResource() { + Optional resourceOption = + Arrays.stream(defaultWriteOptions) + .filter(o -> o.getOptionType() == WriteOption.OptionType.RESOURCE) + .findFirst(); + if (resourceOption.isPresent()) { + return (MonitoredResource) resourceOption.get().getValue(); + } + return null; + } + private LogEntry logEntryFor(LogRecord record) throws Exception { String payload = getFormatter().format(record); Level level = record.getLevel(); @@ -317,7 +363,7 @@ private LogEntry logEntryFor(LogRecord record) throws Exception { @Override public void flush() { try { - getLogging().flush(); + logging.flush(); } catch (Exception ex) { getErrorManager().error(null, ex, ErrorManager.FLUSH_FAILURE); } @@ -348,7 +394,7 @@ public Level getFlushLevel() { */ public void setFlushLevel(Level flushLevel) { this.flushLevel = flushLevel; - getLogging().setFlushSeverity(severityFor(flushLevel)); + logging.setFlushSeverity(severityFor(flushLevel)); } /** @@ -357,12 +403,35 @@ public void setFlushLevel(Level flushLevel) { * @param synchronicity {@link Synchronicity} */ public void setSynchronicity(Synchronicity synchronicity) { - getLogging().setWriteSynchronicity(synchronicity); + logging.setWriteSynchronicity(synchronicity); } /** Get the flush log level. */ public Synchronicity getSynchronicity() { - return getLogging().getWriteSynchronicity(); + return logging.getWriteSynchronicity(); + } + + /** Sets the metadata auto population flag. */ + public void setAutoPopulateMetadata(boolean value) { + this.autoPopulateMetadata = value; + } + + /** Gets the metadata auto population flag. */ + public Boolean getAutoPopulateMetadata() { + return this.autoPopulateMetadata; + } + + /** + * Enable/disable redirection to STDOUT. If set to {@code true}, logs will be printed to STDOUT in + * the Json format that can be parsed by the logging agent. If set to {@code false}, logs will be + * ingested to Cloud Logging by calling Logging API. + */ + public void setRedirectToStdout(boolean value) { + this.redirectToStdout = value; + } + + public Boolean getRedirectToStdout() { + return redirectToStdout; } /** @@ -406,15 +475,7 @@ private static Severity severityFor(Level level) { } } - /** Returns an instance of the logging service. */ - private Logging getLogging() { - if (logging == null) { - synchronized (this) { - if (logging == null) { - logging = loggingOptions.getService(); - } - } - } - return logging; + private static boolean isTrueOrNull(Boolean b) { + return b == null || b == Boolean.TRUE; } } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java index 789f825cb..4404ea675 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java @@ -24,7 +24,9 @@ import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_DESTINATION; import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_NAME; import static com.google.cloud.logging.Logging.WriteOption.OptionType.RESOURCE; +import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.client.util.Strings; import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; @@ -40,7 +42,6 @@ import com.google.cloud.logging.spi.v2.LoggingRpc; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; -import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -92,6 +93,7 @@ class LoggingImpl extends BaseService implements Logging { + protected static final String RESOURCE_NAME_FORMAT = "projects/%s/traces/%s"; private static final int FLUSH_WAIT_TIMEOUT_SECONDS = 6; private final LoggingRpc rpc; private final Map> pendingWrites = new ConcurrentHashMap<>(); @@ -457,10 +459,10 @@ public ApiFuture deleteLogAsync(String log) { @Override public ApiFuture deleteLogAsync(String log, LogDestinationName destination) { - Preconditions.checkNotNull(log, "log parameter cannot be null"); + checkNotNull(log, "log parameter cannot be null"); String projectId = getOptions().getProjectId(); if (destination == null) { - Preconditions.checkNotNull(projectId, "projectId parameter cannot be null"); + checkNotNull(projectId, "projectId parameter cannot be null"); } LogName name = getLogName(projectId, log, destination); DeleteLogRequest request = DeleteLogRequest.newBuilder().setLogName(name.toString()).build(); @@ -790,6 +792,54 @@ private static LogName getLogName( return destination.toLogName(logName); } + public Iterable populateMetadata( + Iterable logEntries, + MonitoredResource customResource, + String... exclusionClassPaths) { + checkNotNull(logEntries); + final Boolean needDebugInfo = + Iterables.any( + logEntries, + log -> log.getSeverity() == Severity.DEBUG && log.getSourceLocation() == null); + final SourceLocation sourceLocation = + needDebugInfo ? SourceLocation.fromCurrentContext(exclusionClassPaths) : null; + // populate monitored resource metadata by prioritizing the one set via + // WriteOption + final MonitoredResource resourceMetadata = + customResource == null + ? MonitoredResourceUtil.getResource(getOptions().getProjectId(), null) + : customResource; + final Context context = (new ContextHandler()).getCurrentContext(); + final ArrayList populatedLogEntries = Lists.newArrayList(); + + // populate empty metadata fields of log entries before calling write API + for (LogEntry entry : logEntries) { + if (entry == null) { + continue; + } + LogEntry.Builder entityBuilder = entry.toBuilder(); + if (resourceMetadata != null && entry.getResource() == null) { + entityBuilder.setResource(resourceMetadata); + } + if (context != null && entry.getHttpRequest() == null) { + entityBuilder.setHttpRequest(context.getHttpRequest()); + } + if (context != null && Strings.isNullOrEmpty(entry.getTrace())) { + MonitoredResource resource = + entry.getResource() != null ? entry.getResource() : resourceMetadata; + entityBuilder.setTrace(getFormattedTrace(context.getTraceId(), resource)); + } + if (context != null && Strings.isNullOrEmpty(entry.getSpanId())) { + entityBuilder.setSpanId(context.getSpanId()); + } + if (entry.getSeverity() == Severity.DEBUG && entry.getSourceLocation() == null) { + entityBuilder.setSourceLocation(sourceLocation); + } + populatedLogEntries.add(entityBuilder.build()); + } + return populatedLogEntries; + } + public void write(Iterable logEntries, WriteOption... options) { if (inWriteCall.get() != null) { return; @@ -797,6 +847,18 @@ public void write(Iterable logEntries, WriteOption... options) { inWriteCall.set(true); try { + final Map writeOptions = optionMap(options); + final Boolean logingOptionsPopulateFlag = getOptions().getAutoPopulateMetadata(); + final Boolean writeOptionPopulateFlga = + WriteOption.OptionType.AUTO_POPULATE_METADATA.get(writeOptions); + + if (writeOptionPopulateFlga == Boolean.TRUE + || (writeOptionPopulateFlga == null && logingOptionsPopulateFlag == Boolean.TRUE)) { + final MonitoredResource sharedResourceMetadata = RESOURCE.get(writeOptions); + logEntries = + populateMetadata(logEntries, sharedResourceMetadata, this.getClass().getName()); + } + writeLogEntries(logEntries, options); if (flushSeverity != null) { for (LogEntry logEntry : logEntries) { @@ -824,6 +886,27 @@ public void flush() { } } + /** + * Formats trace following resource name template if the resource metadata has project id. + * + * @param traceId A trace id string or {@code null} if trace info is missing. + * @param resource A {@see MonitoredResource} describing environment metadata. + * @return A formatted trace id string. + */ + private String getFormattedTrace(String traceId, MonitoredResource resource) { + if (traceId == null) { + return null; + } + String projectId = null; + if (resource != null) { + projectId = resource.getLabels().getOrDefault(MonitoredResourceUtil.PORJECTID_LABEL, null); + } + if (projectId != null) { + return String.format(RESOURCE_NAME_FORMAT, projectId, traceId); + } + return traceId; + } + /* * Write logs synchronously or asynchronously based on writeSynchronicity * setting. diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java index 20c1eb29c..d1d6d1d6b 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java @@ -38,6 +38,8 @@ public class LoggingOptions extends ServiceOptions { private static final String DEFAULT_HOST = LoggingSettings.getDefaultEndpoint(); private static final long serialVersionUID = 5753499510627426717L; + private Boolean autoPopulateMetadataOnWrite = null; + public static class DefaultLoggingFactory implements LoggingFactory { private static final LoggingFactory INSTANCE = new DefaultLoggingFactory(); @@ -72,6 +74,8 @@ protected String getDefaultHost() { public static class Builder extends ServiceOptions.Builder { + private Boolean autoPopulateMetadataOnWrite = true; + private Builder() {} private Builder(LoggingOptions options) { @@ -87,6 +91,11 @@ public Builder setTransportOptions(TransportOptions transportOptions) { return super.setTransportOptions(transportOptions); } + public Builder setAutoPopulateMetadata(boolean autoPopulateMetadataOnWrite) { + this.autoPopulateMetadataOnWrite = autoPopulateMetadataOnWrite; + return this; + } + @Override public LoggingOptions build() { return new LoggingOptions(this); @@ -96,6 +105,7 @@ public LoggingOptions build() { @InternalApi("This class should only be extended within google-cloud-java") protected LoggingOptions(Builder builder) { super(LoggingFactory.class, LoggingRpcFactory.class, builder, new LoggingDefaults()); + this.autoPopulateMetadataOnWrite = builder.autoPopulateMetadataOnWrite; } @SuppressWarnings("serial") @@ -130,6 +140,10 @@ protected LoggingRpc getLoggingRpcV2() { return (LoggingRpc) getRpc(); } + public Boolean getAutoPopulateMetadata() { + return this.autoPopulateMetadataOnWrite; + } + @Override public boolean equals(Object obj) { return obj instanceof LoggingOptions && baseEquals((LoggingOptions) obj); diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java index 098cfbc60..df450aee4 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java @@ -33,6 +33,7 @@ public class MonitoredResourceUtil { private static final String APPENGINE_LABEL_PREFIX = "appengine.googleapis.com/"; + protected static final String PORJECTID_LABEL = Label.ProjectId.getKey(); protected enum Label { ClusterName("cluster_name"), diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/SourceLocation.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/SourceLocation.java index df9126687..14b6f2ab2 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/SourceLocation.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/SourceLocation.java @@ -16,11 +16,11 @@ package com.google.cloud.logging; -import static com.google.common.base.Preconditions.checkElementIndex; - +import com.google.api.client.util.Strings; import com.google.common.base.MoreObjects; import com.google.logging.v2.LogEntrySourceLocation; import java.io.Serializable; +import java.util.Arrays; import java.util.Objects; /** Additional information about the source code location that produced the log entry. */ @@ -158,28 +158,36 @@ static SourceLocation fromPb(LogEntrySourceLocation sourceLocationPb) { } /** - * Creates instance of {@link SourceLocation} based on stack trace information. Caller should - * provide the level in the stack where the information can be located. The stack trace level - * should be {@code 0} to display information for the caller of the method. + * Creates an instance of {@link SourceLocation} based on stack trace information. The stack trace + * level is determined based on the exclusion list of the class paths provided in the {@code + * exclusionClassPaths} parameter. If the list is empty or not defined the caller's stack trace + * information is used. Otherwise, the first {@link StackTraceElement} along the stack which class + * name does not start with any not {@code null} exclusion class paths will be used. * - * @param level Zero-based non-negative integer defining the level in the stack trace where {@code - * 0} is topmost element. + * @param exclusionClassPaths a varargs array of strings containing class path prefixes. * @return a new instance of {@link SourceLocation} populated with file name, method and line * number information. - * @throws IndexOutOfBoundsException if the provided {@link level} is negative or greater than the - * current call stack. */ - static SourceLocation fromCurrentContext(int level) { + static SourceLocation fromCurrentContext(String... exclusionClassPaths) { StackTraceElement[] stackTrace = (new Exception()).getStackTrace(); - Builder builder = newBuilder(); - // need to take info from 1 level down the stack to compensate the call to this - // method - int indexPlus = checkElementIndex(level, stackTrace.length - 1) + 1; - StackTraceElement ste = stackTrace[indexPlus]; - return builder - .setFile(ste.getFileName()) - .setLine(Long.valueOf(ste.getLineNumber())) - .setFunction(ste.getMethodName()) - .build(); + + for (int level = 1; level < stackTrace.length; level++) { + StackTraceElement ste = stackTrace[level]; + String className = ste.getClassName(); + + if (exclusionClassPaths != null) { + if (Strings.isNullOrEmpty(className) + || Arrays.stream(exclusionClassPaths) + .anyMatch(prefix -> prefix != null && className.startsWith(prefix))) { + continue; + } + } + return newBuilder() + .setFile(ste.getFileName()) + .setLine(Long.valueOf(ste.getLineNumber())) + .setFunction(ste.getMethodName()) + .build(); + } + return null; } } diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java new file mode 100644 index 000000000..882df1fd3 --- /dev/null +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.logging; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.newCapture; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.api.core.ApiFutures; +import com.google.cloud.MonitoredResource; +import com.google.cloud.logging.HttpRequest.RequestMethod; +import com.google.cloud.logging.Logging.WriteOption; +import com.google.cloud.logging.spi.LoggingRpcFactory; +import com.google.cloud.logging.spi.v2.LoggingRpc; +import com.google.common.collect.ImmutableList; +import com.google.logging.v2.WriteLogEntriesRequest; +import com.google.logging.v2.WriteLogEntriesResponse; +import org.easymock.Capture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AutoPopulateMetadataTests { + + private static final String LOG_NAME = "test-log"; + private static final String RESOURCE_PROJECT_ID = "env-project-id"; + private static final String LOGGING_PROJECT_ID = "log-project-id"; + private static final MonitoredResource RESOURCE = + MonitoredResource.newBuilder("global") + .addLabel(MonitoredResourceUtil.PORJECTID_LABEL, RESOURCE_PROJECT_ID) + .build(); + private static final LogEntry SIMPLE_LOG_ENTRY = + LogEntry.newBuilder(Payload.StringPayload.of("hello")) + .setLogName(LOG_NAME) + .setDestination(LogDestinationName.project(LOGGING_PROJECT_ID)) + .build(); + private static final LogEntry SIMPLE_LOG_ENTRY_WITH_DEBUG = + LogEntry.newBuilder(Payload.StringPayload.of("hello")) + .setLogName(LOG_NAME) + .setSeverity(Severity.DEBUG) + .setDestination(LogDestinationName.project(LOGGING_PROJECT_ID)) + .build(); + private static final WriteLogEntriesResponse EMPTY_WRITE_RESPONSE = + WriteLogEntriesResponse.newBuilder().build(); + private static final HttpRequest HTTP_REQUEST = + HttpRequest.newBuilder() + .setRequestMethod(RequestMethod.GET) + .setRequestUrl("https://example.com") + .setUserAgent("Test User Agent") + .build(); + private static final String TRACE_ID = "01010101010101010101010101010101"; + private static final String FORMATTED_TRACE_ID = + String.format(LoggingImpl.RESOURCE_NAME_FORMAT, RESOURCE_PROJECT_ID, TRACE_ID); + private static final String SPAN_ID = "1"; + + private LoggingRpcFactory mockedRpcFactory; + private LoggingRpc mockedRpc; + private Logging logging; + private Capture rpcWriteArgument = newCapture(); + private ResourceTypeEnvironmentGetter mockedEnvGetter; + + @Before + public void setup() { + mockedEnvGetter = createMock(ResourceTypeEnvironmentGetter.class); + mockedRpcFactory = createMock(LoggingRpcFactory.class); + mockedRpc = createMock(LoggingRpc.class); + expect(mockedRpcFactory.create(anyObject(LoggingOptions.class))) + .andReturn(mockedRpc) + .anyTimes(); + expect(mockedRpc.write(capture(rpcWriteArgument))) + .andReturn(ApiFutures.immediateFuture(EMPTY_WRITE_RESPONSE)); + MonitoredResourceUtil.setEnvironmentGetter(mockedEnvGetter); + // the following mocks generate MonitoredResource instance same as RESOURCE + // constant + expect(mockedEnvGetter.getAttribute("project/project-id")).andStubReturn(RESOURCE_PROJECT_ID); + expect(mockedEnvGetter.getAttribute("")).andStubReturn(null); + replay(mockedRpcFactory, mockedRpc, mockedEnvGetter); + + LoggingOptions options = + LoggingOptions.newBuilder() + .setProjectId(RESOURCE_PROJECT_ID) + .setServiceRpcFactory(mockedRpcFactory) + .build(); + logging = options.getService(); + } + + @After + public void teardown() { + (new ContextHandler()).removeCurrentContext(); + } + + private void mockCurrentContext(HttpRequest request, String traceId, String spanId) { + Context mockedContext = + Context.newBuilder().setRequest(request).setTraceId(traceId).setSpanId(spanId).build(); + (new ContextHandler()).setCurrentContext(mockedContext); + } + + @Test + public void testAutoPopulationEnabledInLoggingOptions() { + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertEquals(HTTP_REQUEST, actual.getHttpRequest()); + assertEquals(FORMATTED_TRACE_ID, actual.getTrace()); + assertEquals(SPAN_ID, actual.getSpanId()); + assertEquals(RESOURCE, actual.getResource()); + } + + @Test + public void testAutoPopulationEnabledInWriteOptionsAndDisabledInLoggingOptions() { + // redefine logging option to opt out auto-populating + LoggingOptions options = + logging.getOptions().toBuilder().setAutoPopulateMetadata(false).build(); + logging = options.getService(); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.autoPopulateMetadata(true)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertEquals(HTTP_REQUEST, actual.getHttpRequest()); + assertEquals(FORMATTED_TRACE_ID, actual.getTrace()); + assertEquals(SPAN_ID, actual.getSpanId()); + assertEquals(RESOURCE, actual.getResource()); + } + + @Test + public void testAutoPopulationDisabledInWriteOptions() { + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.autoPopulateMetadata(false)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertNull(actual.getHttpRequest()); + assertNull(actual.getTrace()); + assertNull(actual.getSpanId()); + assertNull(actual.getResource()); + } + + @Test + public void testSourceLocationPopulation() { + SourceLocation expected = SourceLocation.fromCurrentContext(); + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY_WITH_DEBUG)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertEquals(expected.getFile(), actual.getSourceLocation().getFile()); + assertEquals(expected.getClass(), actual.getSourceLocation().getClass()); + assertEquals(expected.getFunction(), actual.getSourceLocation().getFunction()); + assertEquals(new Long(expected.getLine() + 1), actual.getSourceLocation().getLine()); + } + + @Test + public void testNotFormattedTraceId() { + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + + final MonitoredResource expectedResource = MonitoredResource.newBuilder("custom").build(); + + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.resource(expectedResource)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertEquals(TRACE_ID, actual.getTrace()); + } + + @Test + public void testMonitoredResourcePopulationInWriteOptions() { + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + + final MonitoredResource expectedResource = MonitoredResource.newBuilder("custom").build(); + + logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.resource(expectedResource)); + + LogEntry actual = LogEntry.fromPb(rpcWriteArgument.getValue().getEntries(0)); + assertEquals(expectedResource, actual.getResource()); + } +} diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/BaseSystemTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/BaseSystemTest.java index 9eea8a181..8c90a3e75 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/BaseSystemTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/BaseSystemTest.java @@ -130,8 +130,6 @@ protected static Iterator waitForLogs(LogName logName) throws Interrup return waitForLogs(logName, null, 1); } - private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - protected static Iterator waitForLogs(Logging.EntryListOption[] options, int minLogs) throws InterruptedException { Page page = logging.listLogEntries(options); diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LogEntryTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LogEntryTest.java index fe10893f1..015bebe23 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LogEntryTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LogEntryTest.java @@ -31,6 +31,7 @@ import java.util.Map; import org.junit.Test; +@SuppressWarnings("deprecation") public class LogEntryTest { private static final String LOG_NAME = "syslog"; @@ -39,8 +40,8 @@ public class LogEntryTest { MonitoredResource.newBuilder("cloudsql_database") .setLabels(ImmutableMap.of("datasetId", "myDataset", "zone", "myZone")) .build(); - private static final Instant TIMESTAMP = Instant.ofEpochMilli(42); - private static final Instant RECEIVE_TIMESTAMP = Instant.ofEpochMilli(24); + private static final Instant TIMESTAMP = Instant.parse("1984-08-13T15:35:30.123Z"); + private static final Instant RECEIVE_TIMESTAMP = Instant.parse("1984-08-13T15:35:31.0Z"); private static final Severity SEVERITY = Severity.ALERT; private static final String INSERT_ID = "insertId"; private static final HttpRequest HTTP_REQUEST = @@ -52,27 +53,13 @@ public class LogEntryTest { ImmutableMap.of("key1", "value1", "key2", "value2"); private static final Operation OPERATION = Operation.of("id", "producer"); private static final String TRACE = "trace"; - private static final Object TRACE_FORMATTER = - new Object() { - @Override - public String toString() { - return TRACE; - } - }; private static final String SPAN_ID = "spanId"; - private static final Object SPAN_ID_FORMATTER = - new Object() { - @Override - public String toString() { - return SPAN_ID; - } - }; private static final boolean TRACE_SAMPLED = true; private static final SourceLocation SOURCE_LOCATION = new SourceLocation.Builder().setFile("file").setLine(42L).setFunction("function").build(); private static final StringPayload STRING_PAYLOAD = StringPayload.of("payload"); private static final JsonPayload JSON_PAYLOAD = - JsonPayload.of(ImmutableMap.of("key", "val")); + JsonPayload.of(ImmutableMap.of("key1", "val", "key2", 123, "key3", false)); private static final ProtoPayload PROTO_PAYLOAD = ProtoPayload.of(Any.pack(Empty.getDefaultInstance())); private static final LogDestinationName BILLING_NAME = @@ -91,8 +78,8 @@ public String toString() { .setHttpRequest(HTTP_REQUEST) .setLabels(LABELS) .setOperation(OPERATION) - .setTrace(TRACE_FORMATTER) - .setSpanId(SPAN_ID_FORMATTER) + .setTrace(TRACE) + .setSpanId(SPAN_ID) .setTraceSampled(TRACE_SAMPLED) .setSourceLocation(SOURCE_LOCATION) .build(); @@ -107,8 +94,8 @@ public String toString() { .setHttpRequest(HTTP_REQUEST) .setLabels(LABELS) .setOperation(OPERATION) - .setTrace(TRACE_FORMATTER) - .setSpanId(SPAN_ID_FORMATTER) + .setTrace(TRACE) + .setSpanId(SPAN_ID) .setTraceSampled(TRACE_SAMPLED) .setSourceLocation(SOURCE_LOCATION) .build(); @@ -123,8 +110,8 @@ public String toString() { .setHttpRequest(HTTP_REQUEST) .setLabels(LABELS) .setOperation(OPERATION) - .setTrace(TRACE_FORMATTER) - .setSpanId(SPAN_ID_FORMATTER) + .setTrace(TRACE) + .setSpanId(SPAN_ID) .setTraceSampled(TRACE_SAMPLED) .setSourceLocation(SOURCE_LOCATION) .build(); @@ -379,4 +366,23 @@ private void compareLogEntry(LogEntry expected, LogEntry value, Boolean extraVal assertEquals(expected.getSourceLocation(), value.getSourceLocation()); assertEquals(expected.getPayload(), value.getPayload()); } + + private static final String[] EXPECTED_STRUCTURED_LOGS = { + "{\"severity\":\"ALERT\",\"timestamp\":\"1984-08-13T15:35:30.123Z\",\"httpRequest\":{\"requestMethod\":\"GET\",\"status\":404,\"cacheLookup\":false,\"cacheHit\":false,\"cacheValidatedWithOriginServer\":false},\"logging.googleapis.com/insertId\":\"insertId\",\"logging.googleapis.com/labels\":{\"key1\":\"value1\",\"key2\":\"value2\"},\"logging.googleapis.com/operation\":{\"id\":\"id\",\"producer\":\"producer\",\"first\":false,\"last\":false},\"logging.googleapis.com/sourceLocation\":{\"file\":\"file\",\"line\":\"42\",\"function\":\"function\"},\"logging.googleapis.com/spanId\":\"spanId\",\"logging.googleapis.com/trace\":\"trace\",\"logging.googleapis.com/trace_sampled\":true,\"message\":\"payload\"}", + "{\"severity\":\"ALERT\",\"timestamp\":\"1984-08-13T15:35:30.123Z\",\"httpRequest\":{\"requestMethod\":\"GET\",\"status\":404,\"cacheLookup\":false,\"cacheHit\":false,\"cacheValidatedWithOriginServer\":false},\"logging.googleapis.com/insertId\":\"insertId\",\"logging.googleapis.com/labels\":{\"key1\":\"value1\",\"key2\":\"value2\"},\"logging.googleapis.com/operation\":{\"id\":\"id\",\"producer\":\"producer\",\"first\":false,\"last\":false},\"logging.googleapis.com/sourceLocation\":{\"file\":\"file\",\"line\":\"42\",\"function\":\"function\"},\"logging.googleapis.com/spanId\":\"spanId\",\"logging.googleapis.com/trace\":\"trace\",\"logging.googleapis.com/trace_sampled\":true,\"key1\":\"val\",\"key2\":123.0,\"key3\":false}" + }; + private static final LogEntry[] TEST_LOG_ENTRIES = {STRING_ENTRY, JSON_ENTRY}; + + @Test + public void testStructureLogPresentations() { + for (int i = 0; i < TEST_LOG_ENTRIES.length; i++) { + String structured_log = TEST_LOG_ENTRIES[i].toStructuredJsonString(); + assertEquals(EXPECTED_STRUCTURED_LOGS[i], structured_log); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testStructureLogPresentationWithProtobufPayload() { + PROTO_ENTRY.toStructuredJsonString(); + } } diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java index af4be4cce..921d58307 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java @@ -19,16 +19,21 @@ import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import com.google.api.client.util.Strings; import com.google.cloud.MonitoredResource; import com.google.cloud.logging.LogEntry.Builder; import com.google.cloud.logging.Logging.WriteOption; import com.google.cloud.logging.Payload.StringPayload; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import java.util.Collections; -import java.util.Map; import java.util.logging.ErrorManager; import java.util.logging.Filter; import java.util.logging.Formatter; @@ -41,6 +46,7 @@ import org.junit.Before; import org.junit.Test; +@SuppressWarnings("deprecation") public class LoggingHandlerTest { private static final String LOG_NAME = "java.log"; @@ -156,18 +162,6 @@ public class LoggingHandlerTest { .setTimestamp(123456789L) .build(); - private static final String CONFIG_NAMESPACE = "com.google.cloud.logging.LoggingHandler"; - private static final ImmutableMap CONFIG_MAP = - ImmutableMap.builder() - .put("log", "testLogName") - .put("level", "ALL") - .put("filter", "com.google.cloud.logging.LoggingHandlerTest$TestFilter") - .put("formatter", "com.google.cloud.logging.LoggingHandlerTest$TestFormatter") - .put("flushLevel", "CRITICAL") - .put("enhancers", "com.google.cloud.logging.LoggingHandlerTest$TestLoggingEnhancer") - .put("resourceType", "testResourceType") - .put("synchronicity", "SYNC") - .build(); private static final ImmutableMap BASE_SEVERITY_MAP = ImmutableMap.of( "levelName", Level.INFO.getName(), "levelValue", String.valueOf(Level.INFO.intValue())); @@ -175,22 +169,9 @@ public class LoggingHandlerTest { new WriteOption[] { WriteOption.logName(LOG_NAME), WriteOption.resource(DEFAULT_RESOURCE), - WriteOption.labels(BASE_SEVERITY_MAP) + WriteOption.labels(BASE_SEVERITY_MAP), }; - private static byte[] renderConfig(Map config) { - StringBuilder str = new StringBuilder(); - for (Map.Entry entry : config.entrySet()) { - str.append(CONFIG_NAMESPACE) - .append('.') - .append(entry.getKey()) - .append('=') - .append(entry.getValue()) - .append(System.lineSeparator()); - } - return str.toString().getBytes(); - } - private Logging logging; private LoggingOptions options; @@ -219,7 +200,14 @@ public void enhanceLogEntry(LogEntry.Builder builder) { @Before public void setUp() { logging = EasyMock.createMock(Logging.class); - options = EasyMock.createStrictMock(LoggingOptions.class); + options = EasyMock.createMock(LoggingOptions.class); + expect(options.getProjectId()).andStubReturn(PROJECT); + expect(options.getService()).andStubReturn(logging); + expect(options.getAutoPopulateMetadata()).andStubReturn(Boolean.FALSE); + logging.setFlushSeverity(EasyMock.anyObject(Severity.class)); + expectLastCall().once(); + logging.setWriteSynchronicity(EasyMock.anyObject(Synchronicity.class)); + expectLastCall().once(); } @After @@ -235,12 +223,6 @@ private static LogRecord newLogRecord(Level level, String message) { @Test public void testPublishLevels() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.write(ImmutableList.of(FINEST_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); logging.write(ImmutableList.of(FINER_ENTRY), DEFAULT_OPTIONS); @@ -290,12 +272,6 @@ public void testPublishLevels() { @Test public void testPublishCustomResource() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); MonitoredResource resource = MonitoredResource.of("custom", ImmutableMap.of()); logging.write( ImmutableList.of(FINEST_ENTRY), @@ -334,12 +310,6 @@ public void testPublishCustomResourceWithProject() { @Test public void testPublishKubernetesContainerResource() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); MonitoredResource resource = MonitoredResource.of( "k8s_container", @@ -369,18 +339,7 @@ public void testPublishKubernetesContainerResource() { @Test public void testEnhancedLogEntry() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - MonitoredResource resource = MonitoredResource.of("custom", ImmutableMap.of()); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); - logging.write( - ImmutableList.of(FINEST_ENHANCED_ENTRY), - WriteOption.logName(LOG_NAME), - WriteOption.resource(resource), - WriteOption.labels(BASE_SEVERITY_MAP)); + logging.write(ImmutableList.of(FINEST_ENHANCED_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); replay(options, logging); LoggingEnhancer enhancer = @@ -391,32 +350,51 @@ public void enhanceLogEntry(Builder builder) { } }; Handler handler = - new LoggingHandler(LOG_NAME, options, resource, Collections.singletonList(enhancer)); + new LoggingHandler( + LOG_NAME, options, DEFAULT_RESOURCE, Collections.singletonList(enhancer)); handler.setLevel(Level.ALL); handler.setFormatter(new TestFormatter()); handler.publish(newLogRecord(Level.FINEST, MESSAGE)); } + @Test + public void testEnhancedLogEntryPrintToStdout() { + final String ExpectedOutput = + "{\"severity\":\"INFO\",\"timestamp\":\"1970-01-02T10:17:36.789Z\",\"logging.googleapis.com/labels\":{\"enhanced\":\"true\"},\"logging.googleapis.com/trace_sampled\":false,\"message\":\"message\"}"; + replay(options, logging); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + + LoggingEnhancer enhancer = + new LoggingEnhancer() { + @Override + public void enhanceLogEntry(Builder builder) { + builder.addLabel("enhanced", "true"); + } + }; + LoggingHandler handler = + new LoggingHandler( + LOG_NAME, options, DEFAULT_RESOURCE, Collections.singletonList(enhancer)); + handler.setLevel(Level.ALL); + handler.setFormatter(new TestFormatter()); + handler.setRedirectToStdout(true); + handler.publish(newLogRecord(Level.INFO, MESSAGE)); + + assertEquals(ExpectedOutput, bout.toString().trim()); // ignore trailing newline! + System.setOut(null); + } + @Test public void testTraceEnhancedLogEntry() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - MonitoredResource resource = MonitoredResource.of("custom", ImmutableMap.of()); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); - logging.write( - ImmutableList.of(TRACE_ENTRY), - WriteOption.logName(LOG_NAME), - WriteOption.resource(resource), - WriteOption.labels(BASE_SEVERITY_MAP)); + logging.write(ImmutableList.of(TRACE_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); replay(options, logging); LoggingEnhancer enhancer = new TraceLoggingEnhancer(); TraceLoggingEnhancer.setCurrentTraceId("projects/projectId/traces/traceId"); Handler handler = - new LoggingHandler(LOG_NAME, options, resource, Collections.singletonList(enhancer)); + new LoggingHandler( + LOG_NAME, options, DEFAULT_RESOURCE, Collections.singletonList(enhancer)); handler.setLevel(Level.ALL); handler.setFormatter(new TestFormatter()); handler.publish(newLogRecord(Level.FINEST, MESSAGE)); @@ -424,13 +402,7 @@ public void testTraceEnhancedLogEntry() { @Test public void testReportWriteError() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); RuntimeException ex = new RuntimeException(); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.write(ImmutableList.of(FINEST_ENTRY), DEFAULT_OPTIONS); expectLastCall().andStubThrow(ex); replay(options, logging); @@ -448,13 +420,7 @@ public void testReportWriteError() { @Test public void testReportFlushError() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); RuntimeException ex = new RuntimeException(); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.write(ImmutableList.of(FINEST_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); logging.flush(); @@ -475,12 +441,6 @@ public void testReportFlushError() { @Test public void testReportFormatError() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); replay(options, logging); Formatter formatter = EasyMock.createStrictMock(Formatter.class); RuntimeException ex = new RuntimeException(); @@ -501,12 +461,6 @@ public void testReportFormatError() { // BUG(1795): rewrite this test when flush actually works. // @Test public void testFlushLevel() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.setFlushSeverity(Severity.WARNING); expectLastCall().once(); logging.write( @@ -529,8 +483,6 @@ public void testFlushLevel() { @Test public void testSyncWrite() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); LogEntry entry = LogEntry.newBuilder(Payload.StringPayload.of(MESSAGE)) .setSeverity(Severity.DEBUG) @@ -539,10 +491,6 @@ public void testSyncWrite() { .setTimestamp(123456789L) .build(); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.setWriteSynchronicity(Synchronicity.SYNC); expectLastCall().once(); logging.write(ImmutableList.of(entry), DEFAULT_OPTIONS); @@ -560,12 +508,6 @@ public void testSyncWrite() { @Test public void testAddHandler() { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().andVoid(); logging.write(ImmutableList.of(FINEST_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); replay(options, logging); @@ -586,12 +528,6 @@ public void close() { @Test public void testClose() throws Exception { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); logging.write(ImmutableList.of(FINEST_ENTRY), DEFAULT_OPTIONS); expectLastCall().once(); logging.close(); @@ -602,17 +538,82 @@ public void testClose() throws Exception { handler.setFormatter(new TestFormatter()); handler.publish(newLogRecord(Level.FINEST, MESSAGE)); handler.close(); - handler.close(); + } + + private void setupOptionsToEnableAutoPopulation() { + reset(options); + options = EasyMock.createMock(LoggingOptions.class); + expect(options.getProjectId()).andStubReturn(PROJECT); + expect(options.getService()).andStubReturn(logging); + expect(options.getAutoPopulateMetadata()).andStubReturn(Boolean.TRUE); + } + + @Test + public void testAutoPopulationEnabled() { + setupOptionsToEnableAutoPopulation(); + // due to the EasyMock bug https://github.com/easymock/easymock/issues/130 + // it is impossible to define expectation for varargs using anyObject() matcher + // the following mock uses the known fact that the method pass two exclusion prefixes + // the following mocks should be replaced with anyObject() matchers when the bug is fixed + expect( + logging.populateMetadata( + EasyMock.eq(ImmutableList.of(INFO_ENTRY)), + EasyMock.eq(DEFAULT_RESOURCE), + EasyMock.anyString(), + EasyMock.anyString())) + .andReturn(ImmutableList.of(INFO_ENTRY)) + .once(); + logging.write(ImmutableList.of(INFO_ENTRY), DEFAULT_OPTIONS); + expectLastCall().once(); + replay(options, logging); + + Handler handler = new LoggingHandler(LOG_NAME, options, DEFAULT_RESOURCE); + handler.setLevel(Level.ALL); + handler.setFormatter(new TestFormatter()); + handler.publish(newLogRecord(Level.INFO, MESSAGE)); + } + + @Test + public void testRedirectToStdoutEnabled() { + setupOptionsToEnableAutoPopulation(); + expect( + logging.populateMetadata( + EasyMock.eq(ImmutableList.of(INFO_ENTRY)), + EasyMock.eq(DEFAULT_RESOURCE), + EasyMock.anyString(), + EasyMock.anyString())) + .andReturn(ImmutableList.of(INFO_ENTRY)) + .once(); + replay(options, logging); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + + LoggingHandler handler = new LoggingHandler(LOG_NAME, options, DEFAULT_RESOURCE); + handler.setLevel(Level.ALL); + handler.setFormatter(new TestFormatter()); + handler.setRedirectToStdout(true); + handler.publish(newLogRecord(Level.INFO, MESSAGE)); + + assertTrue(null, !Strings.isNullOrEmpty(bout.toString())); + System.setOut(null); + } + + @Test + /** Validate that nothing is printed to STDOUT */ + public void testRedirectToStdoutDisabled() { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + + testAutoPopulationEnabled(); + + assertTrue(null, Strings.isNullOrEmpty(bout.toString())); + System.setOut(null); } private void testPublishCustomResourceWithDestination( LogEntry entry, LogDestinationName destination) { - expect(options.getProjectId()).andReturn(PROJECT).anyTimes(); - expect(options.getService()).andReturn(logging); - logging.setFlushSeverity(Severity.ERROR); - expectLastCall().once(); - logging.setWriteSynchronicity(Synchronicity.ASYNC); - expectLastCall().once(); MonitoredResource resource = MonitoredResource.of("custom", ImmutableMap.of()); logging.write( ImmutableList.of(entry), diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingImplTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingImplTest.java index 919cdaa73..68774e720 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingImplTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingImplTest.java @@ -261,6 +261,9 @@ public void setUp() { .setProjectId(PROJECT) .setServiceRpcFactory(rpcFactoryMock) .setRetrySettings(ServiceOptions.getNoRetrySettings()) + // disable auto-population for LoggingImpl class tests + // see {@see AutoPopulationTests} for auto-population tests + .setAutoPopulateMetadata(false) .build(); // By default when calling ListLogEntries, we append a filter of last 24 hours. diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingOptionsTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingOptionsTest.java index c3795e3d3..37f7e64a5 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingOptionsTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingOptionsTest.java @@ -16,21 +16,35 @@ package com.google.cloud.logging; +import static org.easymock.EasyMock.createMock; +import static org.junit.Assert.assertEquals; + import com.google.cloud.TransportOptions; -import org.easymock.EasyMock; -import org.junit.Assert; import org.junit.Test; public class LoggingOptionsTest { + private static final Boolean DONT_AUTO_POPULATE_METADATA = false; + private static final String PROJECT_ID = "fake-project-id"; + + @Test(expected = IllegalArgumentException.class) + public void testNonGrpcTransportOptions() { + TransportOptions invalidTransport = createMock(TransportOptions.class); + LoggingOptions.newBuilder().setTransportOptions(invalidTransport); + } + + @Test + public void testAutoPopulateMetadataOption() { + LoggingOptions actual = + LoggingOptions.newBuilder() + .setProjectId(PROJECT_ID) + .setAutoPopulateMetadata(DONT_AUTO_POPULATE_METADATA) + .build(); + assertEquals(DONT_AUTO_POPULATE_METADATA, actual.getAutoPopulateMetadata()); + } @Test - public void testInvalidTransport() { - try { - LoggingOptions.newBuilder() - .setTransportOptions(EasyMock.createMock(TransportOptions.class)); - Assert.fail(); - } catch (IllegalArgumentException expected) { - Assert.assertNotNull(expected.getMessage()); - } + public void testAutoPopulateMetadataDefaultOption() { + LoggingOptions actual = LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build(); + assertEquals(Boolean.TRUE, actual.getAutoPopulateMetadata()); } } diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingTest.java index 2b7dc0f0e..fab23f972 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingTest.java @@ -43,6 +43,7 @@ public class LoggingTest { private static final String FOLDER_NAME = "folder"; private static final String ORGANIZATION_NAME = "organization"; private static final String BILLING_NAME = "billing"; + private static final Boolean DONT_AUTO_POPULATE_METADATA = false; @Test public void testListOption() { @@ -109,6 +110,10 @@ public void testWriteOption() { writeOption = WriteOption.resource(RESOURCE); assertEquals(RESOURCE, writeOption.getValue()); assertEquals(WriteOption.OptionType.RESOURCE, writeOption.getOptionType()); + + writeOption = WriteOption.autoPopulateMetadata(DONT_AUTO_POPULATE_METADATA); + assertEquals(DONT_AUTO_POPULATE_METADATA, writeOption.getValue()); + assertEquals(WriteOption.OptionType.AUTO_POPULATE_METADATA, writeOption.getOptionType()); } @Test diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/SourceLocationTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/SourceLocationTest.java index e9feb2f9f..a49d02127 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/SourceLocationTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/SourceLocationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.logging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import org.junit.Test; @@ -61,22 +62,39 @@ public void testToAndFromPb() { @Test public void testFromCurrentContext() { StackTraceElement expectedData = (new Exception()).getStackTrace()[0]; - SourceLocation data = SourceLocation.fromCurrentContext(0); + SourceLocation data = SourceLocation.fromCurrentContext(); assertEquals(expectedData.getFileName(), data.getFile()); assertEquals(expectedData.getMethodName(), data.getFunction()); - // mind the assertion is vs (expectedData.lineNumber + 1). it is because the source location - // info of the expectedData is one line above the source location of the tested data. + // mind the assertion is vs (expectedData.lineNumber + 1). it is because the + // source location + // info of the expectedData is one line above the source location of the tested + // data. + assertEquals(Long.valueOf(expectedData.getLineNumber() + 1), data.getLine()); + } + + @Test + public void testFromCurrentContextWithExclusionList() { + StackTraceElement expectedData = (new Exception()).getStackTrace()[0]; + SourceLocation data = SourceLocation.fromCurrentContext(LoggingImpl.class.getName()); + assertEquals(expectedData.getFileName(), data.getFile()); + assertEquals(expectedData.getMethodName(), data.getFunction()); + // mind the assertion is vs (expectedData.lineNumber + 1). it is because the + // source location + // info of the expectedData is one line above the source location of the tested + // data. assertEquals(Long.valueOf(expectedData.getLineNumber() + 1), data.getLine()); } - @Test(expected = IndexOutOfBoundsException.class) public void testFromCurrentContextWithNegativeLevel() { - SourceLocation.fromCurrentContext(-1); + SourceLocation data = SourceLocation.fromCurrentContext((String[]) null); + assertNull(data); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testFromCurrentContextWithVeryLargeLevel() { - SourceLocation.fromCurrentContext(10000); + SourceLocation data = + SourceLocation.fromCurrentContext("com.google", "sun", "java", "jdk", "org"); + assertNull(data); } private void compareSourceLocation(SourceLocation expected, SourceLocation value) {