diff --git a/README.md b/README.md
index 41a2f4eb..097c7681 100644
--- a/README.md
+++ b/README.md
@@ -112,15 +112,90 @@ java $JAVA_OPTS \
-jar $JETTY_HOME/start.jar \
"$@"
```
+## Logging
+This image is configured to use [Java Util Logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html)(JUL) to capture all logging from
+the container and its dependencies. Applications that also use the JUL API will inherit the same logging configuration.
-The configuration of the jetty container in this image can be viewed by running the image locally:
+By default JUL is configured to use a [ConsoleHandler](https://docs.oracle.com/javase/8/docs/api/java/util/logging/ConsoleHandler.html) to send logs to the `stderr` of the container process. When run on as a GCP deployment, all output to `stderr` is captured and is available via the Stackdriver logging console, however more detailed and integrated logs are available if the Stackdriver logging mechanism is used directly (see below).
+
+To alter logging configuration a new `logging.properties` file must be provided to the image that among other things can: alter log levels generated by Loggers; alter log levels accepted by handlers; add/remove/configure log handlers.
+
+
+### Providing `logging.properties` via the web application
+A new logging configuration file can be provided as part of the application (typically at `WEB-INF/logging.properties`)
+and the Java System Property `java.util.logging.config.file` updated to reference it.
+
+When running in a GCP environment, the system property can be set in `app.yaml`:
+```yaml
+env_variables:
+ JAVA_USER_OPTS: -Djava.util.logging.config.file=WEB-INF/logging.properties
```
-docker run --rm -it launcher.gcr.io/google/jetty --list-config --list-modules
+
+If the image is run directly, then a `-e` argument to the `docker run` command can be used to set the system property:
+
+```bash
+docker run \
+ -e JAVA_USER_OPTS=-Djava.util.logging.config.file=WEB-INF/logging.properties \
+ ...
+```
+
+### Providing `logging.properties` via a custom image
+If this image is being used as the base of a custom image, then the following `Dockerfile` commands can be used to add either replace the existing logging configuration file or to add a new `logging.properties` file.
+
+The default logging configuration file is located at `/var/lib/jetty/etc/java-util-logging.properties`, which can be replaced in a custom image is built. The default configuration can be replaced with a `Dockerfile` like:
+
+```Dockerfile
+FROM gcr.io/google-appengine/jetty
+ADD logging.properties /var/lib/jetty/etc/java-util-logging.properties
+...
```
+Alternately an entirely new location for the file can be provided and the environment amended in a `Dockerfile` like:
+
+```Dockerfile
+FROM gcr.io/google-appengine/jetty
+ADD logging.properties /etc/logging.properties
+ENV JAVA_USER_OPTS -Djava.util.logging.config.file=/etc/logging.properties
+...
+```
+
+### Providing `logging.properties` via docker run
+A `logging.properties` file may be added to an existing images using the `docker run` command if the deployment environment allows for the run arguments to be modified. The `-v` option can be used to bind a new `logging.properties` file to the running instance and the `-e` option can be used to set the system property to point to it:
+```shell
+docker run -it --rm \
+-v /mylocaldir/logging.properties:/etc/logging.properties \
+-e JAVA_USER_OPTS="-Djava.util.logging.config.file=/etc/logging.properties" \
+...
+```
+
+### Enhanced Stackdriver Logging (BETA!)
+When running on the Google Cloud Platform Flex environment, the Java Util Logging can be configured to send logs to Google Stackdriver Logging by providing a `logging.properties` file that configures a [LoggingHandler](http://googlecloudplatform.github.io/google-cloud-java/0.10.0/apidocs/com/google/cloud/logging/LoggingHandler.html) as follows:
+```
+.level=INFO
+io.grpc.netty.level=INFO
+sun.net.level=INFO
+
+handlers=com.google.cloud.logging.LoggingHandler
+com.google.cloud.logging.LoggingHandler.level=FINE
+com.google.cloud.logging.LoggingHandler.log=gae_app.log
+com.google.cloud.logging.LoggingHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=%3$s: %5$s%6$s
+
+```
+When deployed on the GCP Flex environment, an image so configured will automatically be configured with:
+* a [LabelLoggingEnhancer](https://github.com/GoogleCloudPlatform/google-cloud-java/blob/v0.17.2/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java#L224) instance, that will add labels from the monitored resource to each log entry.
+* a [TraceLoggingEnhancer](https://github.com/GoogleCloudPlatform/google-cloud-java/blob/v0.17.2/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java) instance that will add any trace-id set to each log entry.
+* the `gcp` module will be enabled that configures jetty so that the [setCurrentTraceId](https://github.com/GoogleCloudPlatform/google-cloud-java/blob/v0.17.2/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java#L40) method is called for any thread handling a request.
+
+When deployed in other environments, logging enhancers can be manually configured by setting a comma separated list of class names as the
+`com.google.cloud.logging.LoggingHandler.enhancers` property.
+
+When using Stackdriver logging, it is recommended that `io.grpc` and `sun.net` logging level is kept at INFO level, as both these packages are used by Stackdriver internals and can result in verbose and/or initialisation problems.
+
+
## Extending the image
The image produced by this project may be automatically used/extended by the Cloud SDK and/or App Engine maven plugin.
-Alternately it may be explicitly extended with a custom Dockerfile.
+Alternately it may be explicitly extended with a custom Dockerfile.
The latest released version of this image is available at `launcher.gcr.io/google/jetty`, alternately you may
build and push your own version with the shell commands:
diff --git a/jetty9-base/pom.xml b/jetty9-base/pom.xml
index 95d13ce9..cdcc3a8b 100644
--- a/jetty9-base/pom.xml
+++ b/jetty9-base/pom.xml
@@ -36,6 +36,17 @@
jar
provided
+
+ com.google.cloud
+ google-cloud-logging
+ ${gcloud.api.version}
+
+
+ commons-logging
+ commons-logging
+
+
+
@@ -96,7 +107,7 @@
gcp-jars
- pre-integration-test
+ package
copy-dependencies
@@ -118,7 +129,7 @@
add-jetty-modules
- pre-integration-test
+ package
exec
@@ -139,7 +150,7 @@
maven-assembly-plugin
- pre-integration-test
+ package
single
diff --git a/jetty9-base/src/main/assembly/assembly.xml b/jetty9-base/src/main/assembly/assembly.xml
index 5acda39d..e2578fe2 100644
--- a/jetty9-base/src/main/assembly/assembly.xml
+++ b/jetty9-base/src/main/assembly/assembly.xml
@@ -33,12 +33,5 @@
**/META-INF/**
-
- ${project.build.directory}
- jetty-base/lib/gcp
-
- ${artifactId}-${version}.jar
-
-
diff --git a/jetty9-base/src/main/java/com/google/cloud/runtimes/jetty9/RequestContextScope.java b/jetty9-base/src/main/java/com/google/cloud/runtimes/jetty9/RequestContextScope.java
new file mode 100644
index 00000000..cb7708b6
--- /dev/null
+++ b/jetty9-base/src/main/java/com/google/cloud/runtimes/jetty9/RequestContextScope.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.cloud.runtimes.jetty9;
+
+import com.google.cloud.logging.TraceLoggingEnhancer;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.handler.ContextHandler.ContextScopeListener;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A Jetty {@link ContextScopeListener} that is called whenever
+ * a container managed thread enters or exits the scope of a context and/or request.
+ * Used to maintain {@link ThreadLocal} references to the current request and
+ * Google traceID, primarily for logging.
+ * @see TracingLogHandler
+ */
+public class RequestContextScope implements ContextHandler.ContextScopeListener {
+ static final Logger logger = Logger.getLogger(RequestContextScope.class.getName());
+
+ private static final String X_CLOUD_TRACE = "x-cloud-trace-context";
+ private static final ThreadLocal contextDepth = new ThreadLocal<>();
+
+ @Override
+ public void enterScope(Context context, Request request, Object reason) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("enterScope " + context);
+ }
+ if (request != null) {
+ Integer depth = contextDepth.get();
+ if (depth == null || depth.intValue() == 0) {
+ depth = 1;
+ String traceId = (String) request.getAttribute(X_CLOUD_TRACE);
+ if (traceId == null) {
+ traceId = request.getHeader(X_CLOUD_TRACE);
+ if (traceId != null) {
+ int slash = traceId.indexOf('/');
+ if (slash >= 0) {
+ traceId = traceId.substring(0, slash);
+ }
+ request.setAttribute(X_CLOUD_TRACE, traceId);
+ TraceLoggingEnhancer.setCurrentTraceId(traceId);
+ }
+ } else {
+ depth = depth + 1;
+ }
+ contextDepth.set(depth);
+ }
+ }
+ }
+
+ @Override
+ public void exitScope(Context context, Request request) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("exitScope " + context);
+ }
+ Integer depth = contextDepth.get();
+ if (depth != null) {
+ if (depth > 1) {
+ contextDepth.set(depth - 1);
+ } else {
+ contextDepth.remove();
+ TraceLoggingEnhancer.setCurrentTraceId(null);
+ }
+ }
+ }
+}
diff --git a/jetty9-base/src/main/jetty-base/config-scripts/jetty.commands b/jetty9-base/src/main/jetty-base/config-scripts/jetty.commands
index 6d3d9d1b..c48fa1df 100644
--- a/jetty9-base/src/main/jetty-base/config-scripts/jetty.commands
+++ b/jetty9-base/src/main/jetty-base/config-scripts/jetty.commands
@@ -1,4 +1,4 @@
--create-startd
-
---add-to-start=server,webapp,http,deploy,jsp,jstl,resources
+--approve-all-licenses
+--add-to-start=server,webapp,http,deploy,jsp,jstl,resources,logging-jul,jcl-slf4j
diff --git a/jetty9-base/src/main/jetty-base/etc/gcp-web.xml b/jetty9-base/src/main/jetty-base/etc/gcp-web.xml
index 4650f4a3..ad30b12e 100644
--- a/jetty9-base/src/main/jetty-base/etc/gcp-web.xml
+++ b/jetty9-base/src/main/jetty-base/etc/gcp-web.xml
@@ -1,10 +1,15 @@
-
+
true
+
+
+
+
+
@@ -16,6 +21,5 @@
/etc/gcp-override-web.xml
-
+
-
diff --git a/jetty9-base/src/main/jetty-base/etc/java-util-logging.properties b/jetty9-base/src/main/jetty-base/etc/java-util-logging.properties
new file mode 100644
index 00000000..3f60ca3f
--- /dev/null
+++ b/jetty9-base/src/main/jetty-base/etc/java-util-logging.properties
@@ -0,0 +1,14 @@
+.level=INFO
+
+handlers=java.util.logging.ConsoleHandler
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=%3$s: %5$s%6$s%n
+
+## To use Stackdriver Logging replace the configuration above with the configuration below:
+# handlers=com.google.cloud.logging.LoggingHandler
+# com.google.cloud.logging.LoggingHandler.level=FINE
+# com.google.cloud.logging.LoggingHandler.log=gae_app.log
+# com.google.cloud.logging.LoggingHandler.resourceType=gae_app
+# com.google.cloud.logging.LoggingHandler.enhancers=com.google.cloud.logging.GaeFlexLoggingEnhancer
+# com.google.cloud.logging.LoggingHandler.formatter=java.util.logging.SimpleFormatter
+# java.util.logging.SimpleFormatter.format=%3$s: %5$s%6$s
diff --git a/jetty9-base/src/main/jetty-base/modules/gcp.mod b/jetty9-base/src/main/jetty-base/modules/gcp.mod
index c5b53247..34da110e 100644
--- a/jetty9-base/src/main/jetty-base/modules/gcp.mod
+++ b/jetty9-base/src/main/jetty-base/modules/gcp.mod
@@ -44,3 +44,6 @@ jetty.httpConfig.sendServerVersion?=true
# Don't send date header
jetty.httpConfig.sendDateHeader?=false
+
+# Hide the gcloud libraries from deployed webapps
+jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/gcp/
diff --git a/jetty9-base/src/test/java/com/google/cloud/runtimes/jetty9/TestServlet.java b/jetty9-base/src/test/java/com/google/cloud/runtimes/jetty9/TestServlet.java
new file mode 100644
index 00000000..e6faf3e1
--- /dev/null
+++ b/jetty9-base/src/test/java/com/google/cloud/runtimes/jetty9/TestServlet.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.cloud.runtimes.jetty9;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+@SuppressWarnings("serial")
+@WebServlet(urlPatterns = {"/", "/test/*"}, name = "TestServlet")
+public class TestServlet extends HttpServlet {
+ static final Logger log = Logger.getLogger(TestServlet.class.getName());
+
+ @PostConstruct
+ private void myPostConstructMethod() {
+ log.info("preconstructed");
+ }
+
+ @Override
+ public void init() throws ServletException {
+ log.info("init info");
+ getServletContext().log("init ServletContext.log");
+ }
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ log.info("doGet info");
+ getServletContext().log("doGet ServletContext.log");
+
+ if (request.getParameter("ex") != null) {
+ try {
+ throw (Throwable) Class.forName(request.getParameter("ex")).newInstance();
+ } catch (ServletException | IOException ex) {
+ throw ex;
+ } catch (Throwable th) {
+ throw new ServletException(th);
+ }
+ }
+ response.getWriter().println("Log Test");
+ }
+}
diff --git a/jetty9/src/main/docker/50-jetty.bash b/jetty9/src/main/docker/50-jetty.bash
index 20ef84b9..ed9b2a64 100644
--- a/jetty9/src/main/docker/50-jetty.bash
+++ b/jetty9/src/main/docker/50-jetty.bash
@@ -19,15 +19,14 @@ if [ ! -e "$ROOT_DIR" -a -d /app ]; then
ln -s /app "$ROOT_DIR"
fi
-# move command line args to $JETTY_ARGS
-export JETTY_ARGS="${@/$1/}"
-set - "$1"
-
-# Check for start.jar
-if [ "$(echo $JETTY_ARGS | egrep start.jar | wc -l )" = "0" ]; then
- JETTY_ARGS="-Djetty.base=${JETTY_BASE} -jar ${JETTY_HOME}/start.jar $JETTY_ARGS"
+# If a webapp root directory exist, use it as the work directory
+if [ -d "$ROOT_DIR" ]; then
+ cd "$ROOT_DIR"
fi
+# Calculate the Jetty args
+export JETTY_ARGS
+
# Add any Jetty properties to the JETTY_ARGS
if [ "$JETTY_PROPERTIES" ]; then
JETTY_ARGS="$JETTY_ARGS ${JETTY_PROPERTIES//,/ }"
@@ -52,7 +51,18 @@ if [ "$PLATFORM" = "gae" ]; then
JETTY_ARGS="$JETTY_ARGS --module=gcp"
fi
-# add the JETTY_ARGS to the JAVA_OPTS
-export JAVA_OPTS="$JAVA_OPTS $JETTY_ARGS"
+# If command line is running java, then assume it is jetty and mix in JETTY_ARGS
+if [ "$1" = "java" ]; then
+ # Check for jetty start.jar and prepend if missing
+ if [ "$(echo $@ | egrep start.jar | wc -l )" = "0" ]; then
+ shift
+ set -- java -Djetty.base=${JETTY_BASE} -jar ${JETTY_HOME}/start.jar $@
+ fi
+ # Append JETTY_ARGS
+ if [ -n "$JETTY_ARGS" ]; then
+ shift
+ set -- java $@ $JETTY_ARGS
+ fi
+fi
diff --git a/jetty9/src/test/workspace/jetty-setup-app.bash b/jetty9/src/test/workspace/jetty-setup-app.bash
index 0df6291b..5324af63 100755
--- a/jetty9/src/test/workspace/jetty-setup-app.bash
+++ b/jetty9/src/test/workspace/jetty-setup-app.bash
@@ -2,6 +2,7 @@
rm -fr $JETTY_BASE/webapps/root $JETTY_BASE/webapps/root.war
mkdir /app
+trap "rm -rf /app" EXIT
echo original > /app/index.html
source /setup-env.d/50-jetty.bash
diff --git a/jetty9/src/test/workspace/jetty-setup-default-gcp.bash b/jetty9/src/test/workspace/jetty-setup-default-gcp.bash
index 52b31794..d6277802 100755
--- a/jetty9/src/test/workspace/jetty-setup-default-gcp.bash
+++ b/jetty9/src/test/workspace/jetty-setup-default-gcp.bash
@@ -1,12 +1,12 @@
#!/bin/bash
-set - zero one two three
+set - java -zero one two three
JAVA_OPTS="-java -options"
PLATFORM=gae
source /setup-env.d/50-jetty.bash
-if [ "$(echo ${JETTY_ARGS} | xargs)" != "-Djetty.base=/var/lib/jetty -jar /opt/jetty-home/start.jar one two three --module=gcp" ]; then
+if [ "$(echo ${JETTY_ARGS} | xargs)" != "--module=gcp" ]; then
echo "JETTY_ARGS='$(echo ${JETTY_ARGS} | xargs)'"
-elif [ "$(echo ${JAVA_OPTS} | xargs)" != "-java -options -Djetty.base=/var/lib/jetty -jar /opt/jetty-home/start.jar one two three --module=gcp" ]; then
- echo "JAVA_OPTS='$(echo ${JAVA_OPTS} | xargs)'"
+elif [ "$(echo $@ | xargs)" != "java -Djetty.base=/var/lib/jetty -jar /opt/jetty-home/start.jar -zero one two three --module=gcp" ]; then
+ echo "@='$(echo $@ | xargs)'"
else
echo OK
fi
diff --git a/jetty9/src/test/workspace/jetty-setup-default.bash b/jetty9/src/test/workspace/jetty-setup-default.bash
index 04a018b5..2592b6f0 100755
--- a/jetty9/src/test/workspace/jetty-setup-default.bash
+++ b/jetty9/src/test/workspace/jetty-setup-default.bash
@@ -2,10 +2,10 @@
set - java start.jar two three
JAVA_OPTS="-java -options"
source /setup-env.d/50-jetty.bash
-if [ "$(echo ${JETTY_ARGS} | xargs)" != "start.jar two three" ]; then
- echo "JETTY_ARGS='${JETTY_ARGS}'"
-elif [ "$(echo ${JAVA_OPTS} | xargs)" != "-java -options start.jar two three" ]; then
- echo "JAVA_OPTS='${JAVA_OPTS}'"
+if [ "$(echo ${JETTY_ARGS} | xargs)" != "" ]; then
+ echo "JETTY_ARGS='$(echo ${JETTY_ARGS} | xargs)'"
+elif [ "$(echo $@ | xargs)" != "java start.jar two three" ]; then
+ echo "@='$(echo $@ | xargs)'"
else
echo OK
fi
diff --git a/jetty9/src/test/workspace/jetty-setup-modules.bash b/jetty9/src/test/workspace/jetty-setup-modules.bash
index 82549217..050ed98d 100755
--- a/jetty9/src/test/workspace/jetty-setup-modules.bash
+++ b/jetty9/src/test/workspace/jetty-setup-modules.bash
@@ -1,6 +1,5 @@
#!/bin/bash
set - java start.jar arg
-JAVA_OPTS="-X"
JETTY_PROPERTIES="prop0=value0,prop1=value1"
JETTY_MODULES_ENABLE="mod0,mod1"
JETTY_MODILES_DISABLE="gone0,gone1"
@@ -9,10 +8,10 @@ touch $JETTY_BASE/start.d/gone0.ini
touch $JETTY_BASE/start.d/gone1.ini
source /setup-env.d/50-jetty.bash
-if [ "$(echo ${JETTY_ARGS} | xargs)" != "start.jar arg prop0=value0 prop1=value1 --module=mod0 --module=mod1" ]; then
- echo "JETTY_ARGS='${JETTY_ARGS}'"
-elif [ "$(echo ${JAVA_OPTS} | xargs)" != "-X start.jar arg prop0=value0 prop1=value1 --module=mod0 --module=mod1" ]; then
- echo "JAVA_OPTS='${JAVA_OPTS}'"
+if [ "$(echo ${JETTY_ARGS} | xargs)" != "prop0=value0 prop1=value1 --module=mod0 --module=mod1" ]; then
+ echo "JETTY_ARGS='$(echo ${JETTY_ARGS} | xargs)'"
+elif [ "$(echo $@ | xargs)" != "java start.jar arg prop0=value0 prop1=value1 --module=mod0 --module=mod1" ]; then
+ echo "@='$(echo $@ | xargs)'"
elif [ -x JETTY_BASE/start.d/gone0.ini ]; then
echo gone0.ini not deleted
elif [ -x JETTY_BASE/start.d/gone1.ini ]; then
diff --git a/jetty9/src/test/workspace/jetty-setup-unpack.bash b/jetty9/src/test/workspace/jetty-setup-unpack.bash
index aa442228..80f62c65 100755
--- a/jetty9/src/test/workspace/jetty-setup-unpack.bash
+++ b/jetty9/src/test/workspace/jetty-setup-unpack.bash
@@ -1,12 +1,14 @@
#!/bin/bash
rm -fr $JETTY_BASE/webapps/root $JETTY_BASE/webapps/root.war
+trap "rm -rf $JETTY_BASE/webapps/root $JETTY_BASE/webapps/root.war" EXIT
mkdir $JETTY_BASE/webapps/root
echo original > $JETTY_BASE/webapps/root/index.html
cd $JETTY_BASE/webapps/root
jar cf ../root.war *
cd ..
+
rm -fr $JETTY_BASE/webapps/root
source /setup-env.d/50-jetty.bash
@@ -31,6 +33,5 @@ if [ "$(cat $JETTY_BASE/webapps/root/index.html)" != "original" ]; then
exit 1
fi
-
echo OK
diff --git a/pom.xml b/pom.xml
index dc9eea0b..1efa9fc5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,9 +29,10 @@
UTF-8
UTF-8
yyyy-MM-dd-HH-mm
+ 1.0.1
4
- 3
- 9.${jetty9.minor.version}.${jetty9.dot.version}.v20170317
+ 5
+ 9.${jetty9.minor.version}.${jetty9.dot.version}.v20170502
${docker.tag.prefix}9.${jetty9.minor.version}
${docker.tag.prefix}9.${jetty9.minor.version}-${maven.build.timestamp}
diff --git a/tests/test-war-smoke/pom.xml b/tests/test-war-smoke/pom.xml
index 389658d3..6b4beef9 100644
--- a/tests/test-war-smoke/pom.xml
+++ b/tests/test-war-smoke/pom.xml
@@ -34,6 +34,12 @@
3.1.0
provided
+
+ com.google.cloud
+ google-cloud-logging
+ ${gcloud.api.version}
+ test
+
com.google.cloud.runtimes.tests
gcloud-testing-core
diff --git a/tests/test-war-smoke/src/main/appengine/app.yaml b/tests/test-war-smoke/src/main/appengine/app.yaml
index fef2315c..53494429 100644
--- a/tests/test-war-smoke/src/main/appengine/app.yaml
+++ b/tests/test-war-smoke/src/main/appengine/app.yaml
@@ -12,3 +12,6 @@ handlers:
- url: /.*
script: ignored
secure: optional
+
+env_variables:
+ JAVA_USER_OPTS: -Djava.util.logging.config.file=WEB-INF/logging.properties
diff --git a/tests/test-war-smoke/src/main/java/com/google/cloud/runtimes/jetty/test/smoke/ClassLoaderServlet.java b/tests/test-war-smoke/src/main/java/com/google/cloud/runtimes/jetty/test/smoke/ClassLoaderServlet.java
new file mode 100644
index 00000000..9562cb5c
--- /dev/null
+++ b/tests/test-war-smoke/src/main/java/com/google/cloud/runtimes/jetty/test/smoke/ClassLoaderServlet.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.cloud.runtimes.jetty.test.smoke;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Test that classes are correctly hidden from webapp.
+ */
+@WebServlet(urlPatterns = {"/classloader"})
+public class ClassLoaderServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ resp.setContentType("text/plain");
+
+ String[] hidden = {
+ "com.google.cloud.logging.Logging",
+ "org.eclipse.jetty.server.Server",
+ "com.google.cloud.BaseService",
+ "io.netty.channel.Channel",
+ "io.netty.util.Timer",
+ "org.slf4j.Logger"
+ };
+
+ int notFound = 0;
+ int found = 0;
+
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ for (String name : hidden) {
+ try {
+ Class> clazz = loader.loadClass(name);
+ resp.getWriter().printf("Context loaded %s from%s%n", name, clazz.getClassLoader());
+ found++;
+ } catch (ClassNotFoundException e) {
+ resp.getWriter().printf("Context Not Found %s%n", name);
+ notFound++;
+ }
+ }
+
+ resp.getWriter().printf("Found classes = %s (0 expected)%n", found);
+ resp.getWriter().printf("Not found classes = %s (%d expected)%n", notFound, hidden.length);
+ }
+}
diff --git a/tests/test-war-smoke/src/main/webapp/WEB-INF/logging.properties b/tests/test-war-smoke/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 00000000..267ac4d5
--- /dev/null
+++ b/tests/test-war-smoke/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,10 @@
+.level=INFO
+org.eclipse.jetty.http.level=INFO
+io.grpc.netty.level=INFO
+sun.net.level=INFO
+
+handlers=com.google.cloud.logging.LoggingHandler
+com.google.cloud.logging.LoggingHandler.level=FINE
+com.google.cloud.logging.LoggingHandler.log=gae_app.log
+com.google.cloud.logging.LoggingHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=%3$s: %5$s%6$s
diff --git a/tests/test-war-smoke/src/test/java/com/google/cloud/runtimes/jetty/test/smoke/LoggingIntegrationTest.java b/tests/test-war-smoke/src/test/java/com/google/cloud/runtimes/jetty/test/smoke/LoggingIntegrationTest.java
new file mode 100644
index 00000000..dd82ad4e
--- /dev/null
+++ b/tests/test-war-smoke/src/test/java/com/google/cloud/runtimes/jetty/test/smoke/LoggingIntegrationTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.cloud.runtimes.jetty.test.smoke;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.google.api.gax.paging.Page;
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.Logging;
+import com.google.cloud.logging.Logging.EntryListOption;
+import com.google.cloud.logging.LoggingOptions;
+import com.google.cloud.logging.Severity;
+import com.google.cloud.runtime.jetty.test.AbstractIntegrationTest;
+import com.google.cloud.runtime.jetty.test.annotation.RemoteOnly;
+import com.google.cloud.runtime.jetty.util.HttpUrlUtil;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.StringReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class LoggingIntegrationTest extends AbstractIntegrationTest {
+
+
+ @Test
+ @RemoteOnly
+ public void testLogging() throws Exception {
+ // Create unique ID to relate request with log entries
+ String id = Long.toHexString(System.nanoTime());
+
+ // Hit the DumpServlet that will log the request path
+ URI target = getUri().resolve("/dump/info/" + id);
+ HttpURLConnection http = HttpUrlUtil.openTo(target);
+ assertThat(http.getResponseCode(), is(200));
+ String responseBody = HttpUrlUtil.getResponseBody(http);
+
+ List lines =
+ new BufferedReader(new StringReader(responseBody)).lines().collect(Collectors.toList());
+ assertThat(lines.stream().filter(s -> s.startsWith("requestURI=")).findFirst().get(),
+ containsString(id));
+ String traceId = lines.stream().filter(s -> s.startsWith("X-Cloud-Trace-Context: ")).findFirst()
+ .get().split("[ /]")[1];
+ assertThat(traceId, Matchers.notNullValue());
+
+ LoggingOptions options = LoggingOptions.getDefaultInstance();
+
+ String filter =
+ "resource.type=gae_app AND resource.labels.module_id=smoke" + " AND textPayload:" + id;
+
+ int expected = 2;
+ try (Logging logging = options.getService()) {
+ Page entries = logging.listLogEntries(EntryListOption.filter(filter));
+ for (LogEntry entry : entries.iterateAll()) {
+ if (entry.getSeverity() == Severity.INFO) {
+ assertThat(entry.getLogName(), is("gae_app.log"));
+ assertThat(entry.getResource().getType(), is("gae_app"));
+ assertThat(entry.getResource().getLabels().get("module_id"), is("smoke"));
+ assertThat(entry.getResource().getLabels().get("zone"), Matchers.notNullValue());
+ assertThat(entry.getLabels().get("appengine.googleapis.com/trace_id"), is(traceId));
+ assertThat(entry.getLabels().get("appengine.googleapis.com/instance_name"),
+ Matchers.notNullValue());
+
+ if (entry.getPayload().toString().contains("JUL.info:/dump/info/")) {
+ expected--;
+ assertThat(entry.getPayload().toString(), Matchers.containsString(id));
+ }
+ if (entry.getPayload().toString().contains("ServletContext.log:/dump/info")) {
+ expected--;
+ assertThat(entry.getPayload().toString(), Matchers.containsString(id));
+ }
+ }
+ }
+ assertThat(expected, is(0));
+ }
+ }
+
+ @Test
+ public void testClassPath() throws Exception {
+
+ URI target = getUri().resolve("/classloader");
+ HttpURLConnection http = HttpUrlUtil.openTo(target);
+ assertThat(http.getResponseCode(), is(200));
+ String responseBody = HttpUrlUtil.getResponseBody(http);
+
+ Assert.assertThat(responseBody, Matchers.containsString("Found classes = 0 (0 expected)"));
+ Assert.assertThat(responseBody, Matchers.containsString("Not found classes = 6 (6 expected)"));
+ }
+}