From 32302feabdb48b3bf62fe5c93d81ca4569f28af3 Mon Sep 17 00:00:00 2001 From: Averi Kitsch Date: Tue, 22 Oct 2019 08:31:39 -0700 Subject: [PATCH] [Cloud Run] Add Structured Logging Sample (#1626) * draft * Sample * fix formatting * Add tests * Add build tests for cloud run * Add to config * fix run command * fix build command * clean up * Fix relative path and make Cloud Run quiet. * remove comment * remove kokoro tests --- run/hello-broken/Dockerfile | 4 - run/hello-broken/pom.xml | 5 -- run/image-processing/pom.xml | 18 +++- run/logging-manual/Dockerfile | 36 ++++++++ run/logging-manual/README.md | 17 ++++ run/logging-manual/pom.xml | 89 ++++++++++++++++++ .../main/java/com/example/cloudrun/App.java | 88 ++++++++++++++++++ .../src/main/resources/logback.xml | 21 +++++ .../java/com/example/cloudrun/AppTest.java | 90 +++++++++++++++++++ 9 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 run/logging-manual/Dockerfile create mode 100644 run/logging-manual/README.md create mode 100644 run/logging-manual/pom.xml create mode 100644 run/logging-manual/src/main/java/com/example/cloudrun/App.java create mode 100644 run/logging-manual/src/main/resources/logback.xml create mode 100644 run/logging-manual/src/test/java/com/example/cloudrun/AppTest.java diff --git a/run/hello-broken/Dockerfile b/run/hello-broken/Dockerfile index ad49ce5de75..d4a39fb549e 100644 --- a/run/hello-broken/Dockerfile +++ b/run/hello-broken/Dockerfile @@ -30,10 +30,6 @@ RUN mvn compile assembly:single # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds FROM adoptopenjdk/openjdk11:alpine -# [START run_system_package_alpine] -RUN apk --no-cache add graphviz ttf-ubuntu-font-family -# [END run_system_package_alpine] - # Copy the jar to the production image from the builder stage. COPY --from=builder /app/target/hello-broken-*dependencies.jar /hello-broken.jar diff --git a/run/hello-broken/pom.xml b/run/hello-broken/pom.xml index 0f1e2f73096..24df2e72b81 100644 --- a/run/hello-broken/pom.xml +++ b/run/hello-broken/pom.xml @@ -51,11 +51,6 @@ limitations under the License. slf4j-simple 1.6.4 - - ch.qos.logback.contrib - logback-json-classic - 0.1.5 - junit junit diff --git a/run/image-processing/pom.xml b/run/image-processing/pom.xml index 47a288a4249..94559147b93 100644 --- a/run/image-processing/pom.xml +++ b/run/image-processing/pom.xml @@ -1,11 +1,25 @@ - + 4.0.0 com.example.cloudrun image-processing 0.0.1-SNAPSHOT - + com.google.cloud.samples shared-configuration diff --git a/run/logging-manual/Dockerfile b/run/logging-manual/Dockerfile new file mode 100644 index 00000000000..5eac073810a --- /dev/null +++ b/run/logging-manual/Dockerfile @@ -0,0 +1,36 @@ +# Copyright 2019 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. + +# Use the official maven/Java 11 image to create a build artifact. +# https://hub.docker.com/_/maven +FROM maven:3.6.2-jdk-11-slim as builder + +# Copy local code to the container image. +WORKDIR /app +COPY pom.xml . +COPY src ./src + +# Build a release artifact. +RUN mvn compile assembly:single + +# Use the Official OpenJDK image for a lean production stage of our multi-stage build. +# https://hub.docker.com/r/adoptopenjdk/openjdk11/ +# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds +FROM adoptopenjdk/openjdk11:alpine + +# Copy the jar to the production image from the builder stage. +COPY --from=builder /app/target/logging-manual-*dependencies.jar /logging-manual.jar + +# Run the web service on container startup. +CMD ["java","-jar","/logging-manual.jar"] diff --git a/run/logging-manual/README.md b/run/logging-manual/README.md new file mode 100644 index 00000000000..f8a52bf170d --- /dev/null +++ b/run/logging-manual/README.md @@ -0,0 +1,17 @@ +# Cloud Run Manual Logging Sample + +This sample shows how to send structured logs to Stackdriver Logging. + +Read more about Cloud Run logging in the [Logging How-to Guide](http://cloud.google.com/run/docs/logging). + +For more details on how to work with this sample read the [Google Cloud Run Java Samples README](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/run). + +[![Run in Google Cloud][run_img]][run_link] + +[run_img]: https://storage.googleapis.com/cloudrun/button.svg +[run_link]: https://deploy.cloud.run/?git_repo=https://github.com/GoogleCloudPlatform/java-docs-samples&dir=run/logging-manual + +## Dependencies + +* **Spark**: Web server framework. +* **Junit**: [development] Test running framework. diff --git a/run/logging-manual/pom.xml b/run/logging-manual/pom.xml new file mode 100644 index 00000000000..3bdc1fb492f --- /dev/null +++ b/run/logging-manual/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + com.example.cloudrun + logging-manual + 0.0.1-SNAPSHOT + + + com.google.cloud.samples + shared-configuration + 1.0.11 + + + UTF-8 + 11 + 11 + + + + com.sparkjava + spark-core + 2.8.0 + + + + org.slf4j + slf4j-api + 1.7.5 + + + net.logstash.logback + logstash-logback-encoder + 5.2 + + + ch.qos.logback + logback-classic + 1.2.3 + + + + com.squareup.okhttp3 + okhttp + 4.0.1 + + + junit + junit + 4.11 + test + + + + + + maven-compiler-plugin + 3.8.0 + + + maven-assembly-plugin + + + + com.example.cloudrun.App + + + + jar-with-dependencies + + + + + + diff --git a/run/logging-manual/src/main/java/com/example/cloudrun/App.java b/run/logging-manual/src/main/java/com/example/cloudrun/App.java new file mode 100644 index 00000000000..dd6d103d398 --- /dev/null +++ b/run/logging-manual/src/main/java/com/example/cloudrun/App.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019 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 + * + * 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.example.cloudrun; + +import static net.logstash.logback.argument.StructuredArguments.kv; +import static spark.Spark.get; +import static spark.Spark.port; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class App { + + private static final Logger logger = LoggerFactory.getLogger(App.class); + private static final String project = getProjectId(); + + public static void main(String[] args) { + int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); + port(port); + + get( + "/", + (req, res) -> { + // [START run_manual_logging] + // Build structured log messages as an object. + Object globalLogFields = null; + // Add log correlation to nest all log messages beneath request log in Log Viewer. + String traceHeader = req.headers("x-cloud-trace-context"); + if (traceHeader != null && project != null) { + String trace = traceHeader.split("/")[0]; + globalLogFields = + kv( + "logging.googleapis.com/trace", + String.format("projects/%s/traces/%s", project, trace)); + } + // Create a structured log entry using key value pairs. + logger.error( + "This is the default display field.", + kv("component", "arbitrary-property"), + kv("severity", "NOTICE"), + globalLogFields); + // [END run_manual_logging] + res.status(200); + return "Hello Logger!"; + }); + } + + // Load the project ID from GCP metadata server. + public static String getProjectId() { + OkHttpClient ok = + new OkHttpClient.Builder() + .readTimeout(500, TimeUnit.MILLISECONDS) + .writeTimeout(500, TimeUnit.MILLISECONDS) + .build(); + + String metadataUrl = "http://metadata.google.internal/computeMetadata/v1/project/project-id"; + Request request = + new Request.Builder().url(metadataUrl).addHeader("Metadata-Flavor", "Google").get().build(); + + String project = null; + try { + Response response = ok.newCall(request).execute(); + project = response.body().string(); + } catch (IOException e) { + logger.error("Error getting Project Id", e); + } + return project; + } +} diff --git a/run/logging-manual/src/main/resources/logback.xml b/run/logging-manual/src/main/resources/logback.xml new file mode 100644 index 00000000000..c6f15bac0b5 --- /dev/null +++ b/run/logging-manual/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + + + [ignore] + [ignore] + [ignore] + [ignore] + [ignore] + [ignore] + + + + + + + + diff --git a/run/logging-manual/src/test/java/com/example/cloudrun/AppTest.java b/run/logging-manual/src/test/java/com/example/cloudrun/AppTest.java new file mode 100644 index 00000000000..e56027b7184 --- /dev/null +++ b/run/logging-manual/src/test/java/com/example/cloudrun/AppTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 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 + * + * 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.example.cloudrun; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static spark.Spark.awaitInitialization; +import static spark.Spark.stop; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.net.HttpURLConnection; +import java.net.URL; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import spark.utils.IOUtils; + +public class AppTest { + + private ByteArrayOutputStream bout; + private PrintStream out; + + @BeforeClass + public static void beforeClass() { + App app = new App(); + app.main(new String[] {}); + awaitInitialization(); + } + + @AfterClass + public static void afterClass() { + stop(); + } + + @Before + public void setUp() { + bout = new ByteArrayOutputStream(); + out = new PrintStream(bout); + System.setOut(out); + } + + @Test + public void shouldSucceed() throws IOException { + TestResponse response = executeRequest("GET", "/"); + assertEquals(200, response.status); + assertEquals("Hello Logger!", response.body); + String output = bout.toString(); + assertTrue(output.toString().contains("This is the default display field.")); + assertTrue(output.toString().contains("NOTICE")); + assertTrue(output.toString().contains("arbitrary-property")); + } + + private static TestResponse executeRequest(String method, String path) throws IOException { + URL url = new URL("http://localhost:8080" + path); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(method); + connection.setDoOutput(true); + connection.connect(); + String body = IOUtils.toString(connection.getInputStream()); + return new TestResponse(connection.getResponseCode(), body); + } + + public static class TestResponse { + + public final String body; + public final int status; + + public TestResponse(int status, String body) { + this.status = status; + this.body = body; + } + } +}