commons-codec
diff --git a/appengine-managed-runtime/src/main/java/com/google/apphosting/vmruntime/VmAppLogsWriter.java b/appengine-managed-runtime/src/main/java/com/google/apphosting/vmruntime/VmAppLogsWriter.java
deleted file mode 100644
index 1600c7c..0000000
--- a/appengine-managed-runtime/src/main/java/com/google/apphosting/vmruntime/VmAppLogsWriter.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright 2016 Google Inc. All Rights Reserved.
- *
- * 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
- *
- * http://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.apphosting.vmruntime;
-
-import com.google.appengine.repackaged.com.google.common.base.Stopwatch;
-import com.google.apphosting.api.ApiProxy;
-import com.google.apphosting.api.ApiProxy.ApiConfig;
-import com.google.apphosting.api.ApiProxy.LogRecord;
-import com.google.apphosting.api.logservice.LogServicePb.FlushRequest;
-import com.google.apphosting.api.logservice.LogServicePb.UserAppLogGroup;
-import com.google.apphosting.api.logservice.LogServicePb.UserAppLogLine;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * {@code AppsLogWriter} is responsible for batching application logs
- * for a single request and sending them back to the AppServer via the
- * LogService.Flush API call.
- *
- * The current algorithm used to send logs is as follows:
- *
- * - The code never allows more than {@code byteCountBeforeFlush} bytes of
- * log data to accumulate in the buffer. If adding a new log line
- * would exceed that limit, the current set of logs are removed from it and an
- * asynchronous API call is started to flush the logs before buffering the
- * new line.
- *
- * - If another flush occurs while a previous flush is still
- * pending, the caller will block synchronously until the previous
- * call completed.
- *
- * - When the overall request completes is should call @code{waitForCurrentFlushAndStartNewFlush}
- * and report the flush count as a HTTP response header. The vm_runtime on the appserver
- * will wait for the reported number of log flushes before forwarding the HTTP response
- * to the user.
- *
- *
- * This class is also responsible for splitting large log entries
- * into smaller fragments, which is unrelated to the batching
- * mechanism described above but is necessary to prevent the AppServer
- * from truncating individual log entries.
- *
- *
This class is thread safe and all methods accessing local state are
- * synchronized. Since each request have their own instance of this class the
- * only contention possible is between the original request thread and and any
- * child RequestThreads created by the request through the threading API.
- *
- */
-@Deprecated
-class VmAppLogsWriter {
- private static final Logger logger = Logger.getLogger(VmAppLogsWriter.class.getName());
-
- // (Some constants below package scope for testability)
- static final String LOG_CONTINUATION_SUFFIX = "\n";
- static final int LOG_CONTINUATION_SUFFIX_LENGTH = LOG_CONTINUATION_SUFFIX.length();
- static final String LOG_CONTINUATION_PREFIX = "\n";
- static final int LOG_CONTINUATION_PREFIX_LENGTH = LOG_CONTINUATION_PREFIX.length();
- static final int MIN_MAX_LOG_MESSAGE_LENGTH = 1024;
- // Log flushes generally complete fast (6 ms at the median, 46ms at the 99th percentile).
- static final int LOG_FLUSH_TIMEOUT_MS = 2000;
-
- private final int maxLogMessageLength;
- private final int logCutLength;
- private final int logCutLengthDiv10;
- private final List buffer;
- private final long maxBytesToFlush;
- private long currentByteCount;
- private final int maxSecondsBetweenFlush;
- private int flushCount = 0;
- private Future currentFlush;
- private Stopwatch stopwatch;
-
- /**
- * Construct an AppLogsWriter instance.
- *
- * @param buffer Buffer holding messages between flushes.
- * @param maxBytesToFlush The maximum number of bytes of log message to
- * allow in a single flush. The code flushes any cached logs before
- * reaching this limit. If this is 0, AppLogsWriter will not start
- * an intermediate flush based on size.
- * @param maxLogMessageLength The maximum length of an individual log line.
- * A single log line longer than this will be written as multiple log
- * entries (with the continuation prefix/suffixes added to indicate this).
- * @param maxFlushSeconds The amount of time to allow a log line to sit
- * cached before flushing. Once a log line has been sitting for more
- * than the specified time, all currently cached logs are flushed. If
- * this is 0, no time based flushing occurs.
- * N.B. because we only check the time on a log call, it is possible for
- * a log to stay cached long after the specified time has been reached.
- * Consider this example (assume maxFlushSeconds=60): the app logs a message
- * when the handler starts but then does not log another message for 10
- * minutes. The initial log will stay cached until the second message
- * is logged.
- */
- public VmAppLogsWriter(
- List buffer,
- long maxBytesToFlush,
- int maxLogMessageLength,
- int maxFlushSeconds) {
- this.buffer = buffer;
- this.maxSecondsBetweenFlush = maxFlushSeconds;
-
- if (maxLogMessageLength < MIN_MAX_LOG_MESSAGE_LENGTH) {
- String message =
- String.format(
- "maxLogMessageLength silly small (%s); setting maxLogMessageLength to %s",
- maxLogMessageLength,
- MIN_MAX_LOG_MESSAGE_LENGTH);
- logger.warning(message);
- this.maxLogMessageLength = MIN_MAX_LOG_MESSAGE_LENGTH;
- } else {
- this.maxLogMessageLength = maxLogMessageLength;
- }
- logCutLength = maxLogMessageLength - LOG_CONTINUATION_SUFFIX_LENGTH;
- logCutLengthDiv10 = logCutLength / 10;
-
- // This should never happen, but putting here just in case.
- if (maxBytesToFlush < this.maxLogMessageLength) {
- String message =
- String.format(
- "maxBytesToFlush (%s) smaller than maxLogMessageLength (%s)",
- maxBytesToFlush,
- this.maxLogMessageLength);
- logger.warning(message);
- this.maxBytesToFlush = this.maxLogMessageLength;
- } else {
- this.maxBytesToFlush = maxBytesToFlush;
- }
-
- // Always have a stopwatch even if we're not doing time based flushing
- // to keep code a bit simpler
- stopwatch = Stopwatch.createUnstarted();
- }
-
- /**
- * Add the specified {@link LogRecord} for the current request. If
- * enough space (or in the future, time) has accumulated, an
- * asynchronous flush may be started. If flushes are backed up,
- * this method may block.
- */
- synchronized void addLogRecordAndMaybeFlush(LogRecord fullRecord) {
- for (LogRecord record : split(fullRecord)) {
- UserAppLogLine logLine = new UserAppLogLine();
- logLine.setLevel(record.getLevel().ordinal());
- logLine.setTimestampUsec(record.getTimestamp());
- logLine.setMessage(record.getMessage());
- // Use maxEncodingSize() here because it's faster and accurate
- // enough for us. It uses the maximum possible size for varint
- // values, but the real size of strings.
- int maxEncodingSize = logLine.maxEncodingSize();
- if (maxBytesToFlush > 0 && (currentByteCount + maxEncodingSize) > maxBytesToFlush) {
- logger.info(currentByteCount + " bytes of app logs pending, starting flush...");
- waitForCurrentFlushAndStartNewFlush();
- }
- if (buffer.size() == 0) {
- // We only want to flush once a log message has been around for
- // longer than maxSecondsBetweenFlush. So, we only start the timer
- // when we add the first message so we don't include time when
- // the queue is empty.
- stopwatch.start();
- }
- buffer.add(logLine);
- currentByteCount += maxEncodingSize;
- }
-
- if (maxSecondsBetweenFlush > 0
- && stopwatch.elapsed(TimeUnit.SECONDS) >= maxSecondsBetweenFlush) {
- waitForCurrentFlushAndStartNewFlush();
- }
- }
-
- /**
- * Starts an asynchronous flush. This method may block if flushes
- * are backed up.
- *
- * @return The number of times this AppLogsWriter has initiated a flush.
- */
- synchronized int waitForCurrentFlushAndStartNewFlush() {
- waitForCurrentFlush();
- if (buffer.size() > 0) {
- currentFlush = doFlush();
- }
- return flushCount;
- }
-
- /**
- * Initiates a synchronous flush. This method will always block
- * until any pending flushes and its own flush completes.
- */
- synchronized int flushAndWait() {
- waitForCurrentFlush();
- if (buffer.size() > 0) {
- currentFlush = doFlush();
- waitForCurrentFlush();
- }
- return flushCount;
- }
-
- /**
- * This method blocks until any outstanding flush is completed. This method
- * should be called prior to {@link #doFlush()} so that it is impossible for
- * the appserver to process logs out of order.
- */
- private void waitForCurrentFlush() {
- if (currentFlush != null) {
- logger.info("End of request or previous flush has not yet completed, blocking.");
- try {
- // VMApiProxyDelegate adds 1000 ms extra to the http connection deadline.
- currentFlush.get(
- VmApiProxyDelegate.ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS + LOG_FLUSH_TIMEOUT_MS,
- TimeUnit.MILLISECONDS);
- } catch (InterruptedException ex) {
- logger.warning(
- "Interruped while blocking on a log flush, setting interrupt bit and "
- + "continuing. Some logs may be lost or occur out of order!");
- Thread.currentThread().interrupt();
- } catch (TimeoutException e) {
- logger.log(
- Level.WARNING,
- "Timeout waiting for log flush to complete. "
- + "Log messages may have been lost/reordered!",
- e);
- } catch (ExecutionException ex) {
- logger.log(
- Level.WARNING, "A log flush request failed. Log messages may have been lost!", ex);
- }
- currentFlush = null;
- }
- }
-
- private Future doFlush() {
- UserAppLogGroup group = new UserAppLogGroup();
- for (UserAppLogLine logLine : buffer) {
- group.addLogLine(logLine);
- }
- buffer.clear();
- currentByteCount = 0;
- flushCount++;
- stopwatch.reset();
- FlushRequest request = new FlushRequest();
- request.setLogsAsBytes(group.toByteArray());
- // This assumes that we are always doing a flush from the request
- // thread. See the TODO above.
- ApiConfig apiConfig = new ApiConfig();
- apiConfig.setDeadlineInSeconds(LOG_FLUSH_TIMEOUT_MS / 1000.0);
- return ApiProxy.makeAsyncCall("logservice", "Flush", request.toByteArray(), apiConfig);
- }
-
- /**
- * Because the App Server will truncate log messages that are too
- * long, we want to split long log messages into mutliple messages.
- * This method returns a {@link List} of {@code LogRecord}s, each of
- * which have the same {@link LogRecord#getLevel()} and
- * {@link LogRecord#getTimestamp()} as
- * this one, and whose {@link LogRecord#getMessage()} is short enough
- * that it will not be truncated by the App Server. If the
- * {@code message} of this {@code LogRecord} is short enough, the list
- * will contain only this {@code LogRecord}. Otherwise the list will
- * contain multiple {@code LogRecord}s each of which contain a portion
- * of the {@code message}. Additionally, strings will be
- * prepended and appended to each of the {@code message}s indicating
- * that the message is continued in the following log message or is a
- * continuation of the previous log mesage.
- */
- List split(LogRecord aRecord) {
- // This method is public so it is testable.
- LinkedList theList = new LinkedList();
- String message = aRecord.getMessage();
- if (null == message || message.length() <= maxLogMessageLength) {
- theList.add(aRecord);
- return theList;
- }
- String remaining = message;
- while (remaining.length() > 0) {
- String nextMessage;
- if (remaining.length() <= maxLogMessageLength) {
- nextMessage = remaining;
- remaining = "";
- } else {
- int cutLength = logCutLength;
- boolean cutAtNewline = false;
- // Try to cut the string at a friendly point
- int friendlyCutLength = remaining.lastIndexOf('\n', logCutLength);
- // But only if that yields a message of reasonable length
- if (friendlyCutLength > logCutLengthDiv10) {
- cutLength = friendlyCutLength;
- cutAtNewline = true;
- }
- nextMessage = remaining.substring(0, cutLength) + LOG_CONTINUATION_SUFFIX;
- remaining = remaining.substring(cutLength + (cutAtNewline ? 1 : 0));
- // Only prepend the continuation prefix if doing so would not push
- // the length of the next message over the limit.
- if (remaining.length() > maxLogMessageLength
- || remaining.length() + LOG_CONTINUATION_PREFIX_LENGTH <= maxLogMessageLength) {
- remaining = LOG_CONTINUATION_PREFIX + remaining;
- }
- }
- theList.add(new LogRecord(aRecord, nextMessage));
- }
- return theList;
- }
-
- /**
- * Sets the stopwatch used for time based flushing.
- *
- * This method is not simply visible for testing, it only exists for testing.
- *
- * @param stopwatch The {@link Stopwatch} instance to use.
- */
- void setStopwatch(Stopwatch stopwatch) {
- this.stopwatch = stopwatch;
- }
-
- /**
- * Get the max length of an individual log message.
- *
- * This method is not simply visible for testing, it only exists for testing.
- */
- int getMaxLogMessageLength() {
- return maxLogMessageLength;
- }
-
- /**
- * Get the maximum number of log bytes that can be sent at a single time.
- *
- * This code is not simply visible for testing, it only exists for testing.
- */
- long getByteCountBeforeFlushing() {
- return maxBytesToFlush;
- }
-}
diff --git a/appengine-managed-runtime/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger b/appengine-managed-runtime/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger
new file mode 100644
index 0000000..9187909
--- /dev/null
+++ b/appengine-managed-runtime/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger
@@ -0,0 +1 @@
+com.google.apphosting.logging.CloudLoggingFileHandler
diff --git a/jetty9-base/pom.xml b/jetty9-base/pom.xml
index 5cdd3c3..b76027e 100644
--- a/jetty9-base/pom.xml
+++ b/jetty9-base/pom.xml
@@ -52,6 +52,9 @@
+
+ ${project.basedir}/src/main/resources
+
${project.build.directory}/jetty-base
false
diff --git a/jetty9-base/src/main/jetty-base/etc/gae.xml b/jetty9-base/src/main/jetty-base/etc/gae.xml
index 8f88e3f..ccca81b 100644
--- a/jetty9-base/src/main/jetty-base/etc/gae.xml
+++ b/jetty9-base/src/main/jetty-base/etc/gae.xml
@@ -57,10 +57,7 @@
-
- /request.yyyy_mm_dd.log
- 2
- true
+
true
GMT
true
diff --git a/jetty9-compat-base/pom.xml b/jetty9-compat-base/pom.xml
index cf85a9a..19706a0 100644
--- a/jetty9-compat-base/pom.xml
+++ b/jetty9-compat-base/pom.xml
@@ -140,6 +140,9 @@
+
+ ${project.basedir}/src/main/resources
+
${project.build.directory}/jetty-base
false
diff --git a/jetty9-compat-base/src/main/assembly/assembly.xml b/jetty9-compat-base/src/main/assembly/assembly.xml
index e2578fe..88a4d82 100644
--- a/jetty9-compat-base/src/main/assembly/assembly.xml
+++ b/jetty9-compat-base/src/main/assembly/assembly.xml
@@ -31,6 +31,7 @@
**/META-INF/**
+ logs/**
diff --git a/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/RequestLoggerHandler.java b/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/RequestLoggerHandler.java
new file mode 100644
index 0000000..678e0dd
--- /dev/null
+++ b/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/RequestLoggerHandler.java
@@ -0,0 +1,90 @@
+package com.google.apphosting.vmruntime.jetty9;
+
+import com.google.apphosting.logging.SystemLogger;
+
+import java.io.IOException;
+import java.util.logging.FileHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * The {@link java.util.logging.Handler} is responsible for capturing the
+ * access logging events from {@link org.eclipse.jetty.server.handler.RequestLogHandler}
+ * and {@link org.eclipse.jetty.server.Slf4jRequestLog} to a rolling file
+ * on disk.
+ */
+public class RequestLoggerHandler extends FileHandler implements SystemLogger {
+ public static class RequestLogFormatter extends Formatter {
+ @Override
+ public String format(LogRecord record) {
+ // We don't expand the message, or use the ResourceBundle stuff for this formatter.
+ return record.getMessage();
+ }
+ }
+
+ public static final String LOG_DIRECTORY_PROPERTY = "com.google.apphosting.logs";
+ public static final String LOG_PATTERN_CONFIG_PROPERTY =
+ RequestLoggerHandler.class.getName() + ".pattern";
+ private static final String DEFAULT_LOG_DIRECTORY = "/var/log/app_engine";
+ private static final String DEFAULT_LOG_PATTERN = "access.%g.log";
+
+ private static final int LOG_PER_FILE_SIZE = getConfigInt("limit", 10 * 1024 * 1024);
+ private static final int LOG_MAX_FILES = getConfigInt("count", 3);
+ private static final boolean LOG_APPEND = getConfigBool("append", true);
+
+ public RequestLoggerHandler() throws IOException, SecurityException {
+ super(getFilePattern(), LOG_PER_FILE_SIZE, LOG_MAX_FILES, LOG_APPEND);
+ setFormatter(new RequestLogFormatter());
+ }
+
+ private static boolean getConfigBool(String suffix, boolean defValue) {
+ String val = System.getProperty(RequestLoggerHandler.class.getName() + '.' + suffix);
+ if (val == null) {
+ return defValue;
+ }
+ return Boolean.parseBoolean(val);
+ }
+
+ private static int getConfigInt(String suffix, int defValue) {
+ String val = System.getProperty(RequestLoggerHandler.class.getName() + '.' + suffix);
+ if (val == null) {
+ return defValue;
+ }
+ return Integer.parseInt(val);
+ }
+
+ private static String getFilePattern() {
+ String directory = System.getProperty(LOG_DIRECTORY_PROPERTY, DEFAULT_LOG_DIRECTORY);
+
+ String pattern = System.getProperty(LOG_PATTERN_CONFIG_PROPERTY);
+ if (pattern != null) {
+ if (pattern.startsWith("/")) {
+ return pattern;
+ }
+ return directory + "/" + pattern;
+ }
+
+ return directory + "/" + DEFAULT_LOG_PATTERN;
+ }
+
+ @Override
+ public void configure() throws IOException {
+ // This name is logger name that the Sfl4jRequestLog emits events to
+ Logger logger = Logger.getLogger("org.eclipse.jetty.server.RequestLog");
+ logger.setLevel(Level.INFO);
+
+ // Ensure handler is present
+ // Look for the expected Handlers
+ for (Handler handler : logger.getHandlers()) {
+ if (handler instanceof RequestLoggerHandler) {
+ // Nothing else to do, return
+ return;
+ }
+ }
+
+ logger.addHandler(new RequestLoggerHandler());
+ }
+}
diff --git a/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/VmRuntimeWebAppContext.java b/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/VmRuntimeWebAppContext.java
index a4bac17..8ca3c5c 100644
--- a/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/VmRuntimeWebAppContext.java
+++ b/jetty9-compat-base/src/main/java/com/google/apphosting/vmruntime/jetty9/VmRuntimeWebAppContext.java
@@ -21,6 +21,7 @@
import com.google.appengine.api.memcache.MemcacheSerialization;
import com.google.appengine.spi.ServiceFactoryFactory;
import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.logging.CoreLogging;
import com.google.apphosting.logging.LogContext;
import com.google.apphosting.runtime.DatastoreSessionStore;
import com.google.apphosting.runtime.DeferredDatastoreSessionStore;
@@ -39,7 +40,6 @@
import com.google.apphosting.vmruntime.VmEnvironmentFactory;
import com.google.apphosting.vmruntime.VmMetadataCache;
import com.google.apphosting.vmruntime.VmRequestUtils;
-import com.google.apphosting.vmruntime.VmRuntimeFileLogHandler;
import com.google.apphosting.vmruntime.VmRuntimeUtils;
import com.google.apphosting.vmruntime.VmTimer;
@@ -324,11 +324,12 @@ public void init(String appengineWebXmlFile) throws AppEngineConfigException, IO
appEngineWebXml = appEngineWebXmlReader.readAppEngineWebXml();
}
VmRuntimeUtils.installSystemProperties(defaultEnvironment, appEngineWebXml);
- String logConfig = System.getProperty("java.util.logging.config.file");
+
+ String logConfig = System.getProperty(CoreLogging.JAVA_UTIL_LOGGING_CONFIG_PROPERTY);
if (logConfig != null && logConfig.startsWith("WEB-INF/")) {
- System.setProperty("java.util.logging.config.file", URIUtil.addPaths(appDir, logConfig));
+ logConfig = URIUtil.addPaths(appDir, logConfig);
}
- VmRuntimeFileLogHandler.init();
+ CoreLogging.init(logConfig);
if (appEngineWebXml == null) {
// No need to configure the session manager.
diff --git a/jetty9-compat-base/src/main/jetty-base/etc/gae.xml b/jetty9-compat-base/src/main/jetty-base/etc/gae.xml
index c2265e0..cacb1ae 100644
--- a/jetty9-compat-base/src/main/jetty-base/etc/gae.xml
+++ b/jetty9-compat-base/src/main/jetty-base/etc/gae.xml
@@ -88,17 +88,14 @@
-
-
- /request.yyyy_mm_dd.log
- 2
- true
- true
- GMT
- true
- true
-
-
+
+
+ true
+ GMT
+ true
+ true
+
+
diff --git a/jetty9-compat-base/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger b/jetty9-compat-base/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger
new file mode 100644
index 0000000..07df7b2
--- /dev/null
+++ b/jetty9-compat-base/src/main/resources/META-INF/services/com.google.apphosting.logging.SystemLogger
@@ -0,0 +1 @@
+com.google.apphosting.vmruntime.jetty9.RequestLoggerHandler
\ No newline at end of file
diff --git a/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/JettyRunner.java b/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/JettyRunner.java
index 88fae47..82b932f 100644
--- a/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/JettyRunner.java
+++ b/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/JettyRunner.java
@@ -16,11 +16,11 @@
package com.google.apphosting.vmruntime.jetty9;
-import static com.google.apphosting.vmruntime.VmRuntimeFileLogHandler.JAVA_UTIL_LOGGING_CONFIG_PROPERTY;
+import static com.google.apphosting.logging.CoreLogging.JAVA_UTIL_LOGGING_CONFIG_PROPERTY;
import static com.google.apphosting.vmruntime.jetty9.VmRuntimeTestBase.JETTY_HOME_PATTERN;
import com.google.apphosting.jetty9.GoogleRequestCustomizer;
-import com.google.apphosting.vmruntime.VmRuntimeFileLogHandler;
+import com.google.apphosting.logging.CloudLoggingFileHandler;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.io.MappedByteBufferPool;
@@ -120,9 +120,14 @@ public void doStart() throws Exception {
Assert.assertTrue(target.isDirectory());
Assert.assertTrue(jettyBaseFile.isDirectory());
logs = new File(target, "logs");
- logs.delete();
- logs.mkdirs();
- logs.deleteOnExit();
+ if (!logs.exists()) {
+ logs.mkdirs();
+ /* Note: due to java.util.logging file locks, you cannot:
+ * - delete the log directory or
+ * - delete all of its contents or
+ * - or perform a deleteOnExit() successfully
+ */
+ }
// Set GAE SystemProperties
setSystemProperties(logs);
@@ -259,8 +264,10 @@ public void run() {
* Sets the system properties expected by jetty.xml.
*/
protected void setSystemProperties(File logs) throws IOException {
- String logFilePattern = logs.getAbsolutePath() + "/log.%g";
- System.setProperty(VmRuntimeFileLogHandler.LOG_PATTERN_CONFIG_PROPERTY, logFilePattern);
+ System.setProperty(CloudLoggingFileHandler.LOG_PATTERN_CONFIG_PROPERTY,
+ logs.getAbsolutePath() + "/log%u.%g.json");
+ System.setProperty(RequestLoggerHandler.LOG_PATTERN_CONFIG_PROPERTY,
+ logs.getAbsolutePath() + "/request-%u.%g.log");
System.setProperty("jetty.appengineport", String.valueOf(findAvailablePort()));
System.setProperty("jetty.appenginehost", "localhost");
System.setProperty("jetty.appengine.forwarded", "true");
diff --git a/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/LoggingIT.java b/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/LoggingIT.java
index be97a82..5819ead 100644
--- a/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/LoggingIT.java
+++ b/jetty9-compat-base/src/test/java/com/google/apphosting/vmruntime/jetty9/LoggingIT.java
@@ -17,6 +17,7 @@
package com.google.apphosting.vmruntime.jetty9;
import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import com.google.gson.Gson;
@@ -27,19 +28,73 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class LoggingIT extends VmRuntimeTestBase {
+ /**
+ * Find the log file that contains the specific content pattern.
+ *
+ * This exists to get around the fact that the java.util.logging
+ * layer will generate a new log file each time its reset/initialized
+ * because it sees the lock from the previous instance before the
+ * previous instance has a chance to close and remove the lock.
+ *
+ *
+ * This function will iterate through all of the log files and
+ * attempt to find the specific pattern you are interested in.
+ *
+ *
+ * @param logDir the directory where log files are kept, tracked by
+ * the {@link JettyRunner}
+ * @param regex the text (regex) pattern to look for in the log file
+ * @return the File for the discovered log file
+ * @throws java.io.FileNotFoundException if unable to find a log
+ * file with the specific text pattern
+ * @throws IOException if unable to read a log file and/or pattern
+ */
+ public File findLogFileThatContains(File logDir, String regex) throws IOException {
+ Pattern logPattern = Pattern.compile("log[0-9]+\\.[0-9]+\\.json");
+ Pattern textPattern = Pattern.compile(regex);
+ Matcher logMatcher;
+ String line;
+
+ for (File file : logDir.listFiles()) {
+ logMatcher = logPattern.matcher(file.getName());
+ boolean validFile = logMatcher.matches();
+ if (validFile) {
+ // search file
+ try (FileInputStream in = new FileInputStream(file);
+ InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
+ BufferedReader buf = new BufferedReader(reader)) {
+ Matcher textMatcher;
+ while ((line = buf.readLine()) != null) {
+ textMatcher = textPattern.matcher(line);
+ if (textMatcher.find()) {
+ return file;
+ }
+ }
+ }
+ }
+ }
+
+ throw new FileNotFoundException(
+ "Unable to find pattern [" + textPattern.pattern() + "] in logs at " + logDir);
+ }
+
public void testGet() throws Exception {
//runner.dump();
HttpClient httpClient = new HttpClient();
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000);
- String query = Long.toHexString(System.nanoTime());
- GetMethod get = new GetMethod(createUrl("/testLogging?nano=" + query).toString());
+ String query = "nano=" + Long.toHexString(System.nanoTime());
+ GetMethod get = new GetMethod(createUrl("/testLogging?" + query).toString());
int httpCode = httpClient.executeMethod(get);
assertThat(httpCode, equalTo(200));
@@ -47,34 +102,36 @@ public void testGet() throws Exception {
String body = get.getResponseBodyAsString();
assertThat(body, equalTo("FINE\nSEVERE\nfalse\n\n"));
- File logs = runner.getLogDir();
- File log = new File(logs, "log.0");
+ File log = findLogFileThatContains(runner.getLogDir(),query);
assertTrue(log.exists());
// Look for the log entry with our query string
- try (BufferedReader in =
- new BufferedReader(
- new InputStreamReader(new FileInputStream(log), StandardCharsets.ISO_8859_1))) {
+ try (FileInputStream in = new FileInputStream(log);
+ InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
+ BufferedReader buf = new BufferedReader(reader)) {
String line;
- while ((line = in.readLine()) != null) {
+ while ((line = buf.readLine()) != null) {
if (line.contains(query)) {
break;
}
}
JsonData data = new Gson().fromJson(line, JsonData.class);
+ assertThat("Parsed Data", data, notNullValue());
assertThat(data.severity, equalTo("INFO"));
- assertThat(data.message, org.hamcrest.Matchers.containsString("LogTest Hello nano=" + query));
+ assertThat(data.message, org.hamcrest.Matchers.containsString("LogTest Hello " + query));
- line = in.readLine();
+ line = buf.readLine();
data = new Gson().fromJson(line, JsonData.class);
+ assertThat("Parsed Data", data, notNullValue());
assertThat(data.severity, equalTo("ERROR"));
- assertThat(
- data.message, org.hamcrest.Matchers.containsString("LoggingServlet doGet: not null"));
+ assertThat(data.message,
+ org.hamcrest.Matchers.containsString("LoggingServlet doGet: not null"));
- line = in.readLine();
+ line = buf.readLine();
data = new Gson().fromJson(line, JsonData.class);
+ assertThat("Parsed Data", data, notNullValue());
assertThat(data.severity, equalTo("ERROR"));
assertThat(data.message, org.hamcrest.Matchers.containsString("LoggingServlet doGet: null"));
}
@@ -87,6 +144,7 @@ public static class LogTimestamp {
public long nanos;
}
+
public LogTimestamp timestamp;
public String severity;
public String thread;
diff --git a/pom.xml b/pom.xml
index b43e8c7..df41792 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,6 +95,13 @@
2.3.1
+