Skip to content

Commit 5ef5317

Browse files
authored
feat: Add execution id logging to uniquely identify request logs (#319)
Adds an execution id for each http. When the LOG_EXECUTION_ID env var is set, the execution id is added as a new field to the json logs.
1 parent d6fc4a4 commit 5ef5317

File tree

6 files changed

+150
-6
lines changed

6 files changed

+150
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
# Maven
2222
target/
23+
dependency-reduced-pom.xml
2324

2425
# Gradle
2526
.gradle

invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.cloud.functions.CloudEventsFunction;
2222
import com.google.cloud.functions.Context;
2323
import com.google.cloud.functions.RawBackgroundFunction;
24+
import com.google.cloud.functions.invoker.gcf.ExecutionIdUtil;
2425
import com.google.gson.Gson;
2526
import com.google.gson.GsonBuilder;
2627
import com.google.gson.TypeAdapter;
@@ -51,6 +52,7 @@ public final class BackgroundFunctionExecutor extends HttpServlet {
5152
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
5253

5354
private final FunctionExecutor<?> functionExecutor;
55+
private final ExecutionIdUtil executionIdUtil = new ExecutionIdUtil();
5456

5557
private BackgroundFunctionExecutor(FunctionExecutor<?> functionExecutor) {
5658
this.functionExecutor = functionExecutor;
@@ -323,6 +325,7 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
323325
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
324326
String contentType = req.getContentType();
325327
try {
328+
executionIdUtil.storeExecutionId(req);
326329
if ((contentType != null && contentType.startsWith("application/cloudevents+json"))
327330
|| req.getHeader("ce-specversion") != null) {
328331
serviceCloudEvent(req);
@@ -333,6 +336,8 @@ public void service(HttpServletRequest req, HttpServletResponse res) throws IOEx
333336
} catch (Throwable t) {
334337
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
335338
logger.log(Level.SEVERE, "Failed to execute " + functionExecutor.functionName(), t);
339+
} finally {
340+
executionIdUtil.removeExecutionId();
336341
}
337342
}
338343

@@ -359,7 +364,14 @@ private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exce
359364
// ServiceLoader.load
360365
// will throw ServiceConfigurationError. At this point we're still running with the default
361366
// context ClassLoader, which is the system ClassLoader that has loaded the code here.
362-
runWithContextClassLoader(() -> executor.serviceCloudEvent(reader.toEvent(data -> data)));
367+
try {
368+
executionIdUtil.storeExecutionId(req);
369+
runWithContextClassLoader(() -> executor.serviceCloudEvent(reader.toEvent(data -> data)));
370+
} catch (Throwable t) {
371+
logger.log(Level.SEVERE, "Failed to execute " + executor.functionName(), t);
372+
} finally {
373+
executionIdUtil.removeExecutionId();
374+
}
363375
// The data->data is a workaround for a bug fixed since Milestone 4 of the SDK, in
364376
// https://github.com/cloudevents/sdk-java/pull/259.
365377
}

invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.cloud.functions.invoker;
1616

1717
import com.google.cloud.functions.HttpFunction;
18+
import com.google.cloud.functions.invoker.gcf.ExecutionIdUtil;
1819
import com.google.cloud.functions.invoker.http.HttpRequestImpl;
1920
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
2021
import java.util.logging.Level;
@@ -28,6 +29,7 @@ public class HttpFunctionExecutor extends HttpServlet {
2829
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
2930

3031
private final HttpFunction function;
32+
private final ExecutionIdUtil executionIdUtil = new ExecutionIdUtil();
3133

3234
private HttpFunctionExecutor(HttpFunction function) {
3335
this.function = function;
@@ -68,13 +70,15 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
6870
HttpResponseImpl respImpl = new HttpResponseImpl(res);
6971
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
7072
try {
73+
executionIdUtil.storeExecutionId(req);
7174
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
7275
function.service(reqImpl, respImpl);
7376
} catch (Throwable t) {
7477
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
7578
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
7679
} finally {
7780
Thread.currentThread().setContextClassLoader(oldContextLoader);
81+
executionIdUtil.removeExecutionId();
7882
respImpl.flush();
7983
}
8084
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.google.cloud.functions.invoker.gcf;
2+
3+
import java.util.Base64;
4+
import java.util.Random;
5+
import java.util.concurrent.ThreadLocalRandom;
6+
import java.util.logging.Handler;
7+
import java.util.logging.Logger;
8+
import javax.servlet.http.HttpServletRequest;
9+
10+
/**
11+
* A helper class that either fetches a unique execution id from request HTTP headers or generates a
12+
* random id.
13+
*/
14+
public final class ExecutionIdUtil {
15+
private static final Logger rootLogger = Logger.getLogger("");
16+
private static final int EXECUTION_ID_LENGTH = 12;
17+
private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID";
18+
private static final String LOG_EXECUTION_ID_ENV_NAME = "LOG_EXECUTION_ID";
19+
20+
private final Random random = ThreadLocalRandom.current();
21+
22+
/**
23+
* Add mapping to root logger from current thread id to execution id. This mapping will be used to
24+
* append the execution id to log lines.
25+
*/
26+
public void storeExecutionId(HttpServletRequest request) {
27+
if (!executionIdLoggingEnabled()) {
28+
return;
29+
}
30+
for (Handler handler : rootLogger.getHandlers()) {
31+
if (handler instanceof JsonLogHandler) {
32+
String id = getOrGenerateExecutionId(request);
33+
((JsonLogHandler) handler).addExecutionId(Thread.currentThread().getId(), id);
34+
}
35+
}
36+
}
37+
38+
/** Remove mapping from curent thread to request execution id */
39+
public void removeExecutionId() {
40+
if (!executionIdLoggingEnabled()) {
41+
return;
42+
}
43+
for (Handler handler : rootLogger.getHandlers()) {
44+
if (handler instanceof JsonLogHandler) {
45+
((JsonLogHandler) handler).removeExecutionId(Thread.currentThread().getId());
46+
}
47+
}
48+
}
49+
50+
private String getOrGenerateExecutionId(HttpServletRequest request) {
51+
String executionId = request.getHeader(EXECUTION_ID_HTTP_HEADER);
52+
if (executionId == null) {
53+
byte[] array = new byte[EXECUTION_ID_LENGTH];
54+
random.nextBytes(array);
55+
executionId = Base64.getEncoder().encodeToString(array);
56+
}
57+
return executionId;
58+
}
59+
60+
private boolean executionIdLoggingEnabled() {
61+
return Boolean.parseBoolean(System.getenv().getOrDefault(LOG_EXECUTION_ID_ENV_NAME, "false"));
62+
}
63+
}

invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/JsonLogHandler.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.io.StringWriter;
66
import java.util.ArrayList;
77
import java.util.List;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
810
import java.util.logging.Handler;
911
import java.util.logging.Level;
1012
import java.util.logging.LogRecord;
@@ -24,6 +26,14 @@ public final class JsonLogHandler extends Handler {
2426

2527
private final PrintStream out;
2628
private final boolean closePrintStreamOnClose;
29+
// This map is used to track execution id for currently running Jetty requests. Mapping thread
30+
// id to request works because of an implementation detail of Jetty thread pool handling.
31+
// Jetty worker threads completely handle a request before beginning work on a new request.
32+
// NOTE: Store thread id as a string to avoid comparison failures between int and long.
33+
//
34+
// Jetty Documentation (https://jetty.org/docs/jetty/10/programming-guide/arch/threads.html)
35+
private static final ConcurrentMap<String, String> executionIdByThreadMap =
36+
new ConcurrentHashMap<>();
2737

2838
public JsonLogHandler(PrintStream out, boolean closePrintStreamOnClose) {
2939
this.out = out;
@@ -38,6 +48,7 @@ public void publish(LogRecord record) {
3848
StringBuilder json = new StringBuilder("{");
3949
appendSeverity(json, record);
4050
appendSourceLocation(json, record);
51+
appendExecutionId(json, record);
4152
appendMessage(json, record); // must be last, see appendMessage
4253
json.append("}");
4354
// We must output the log all at once (should only call println once per call to publish)
@@ -96,6 +107,12 @@ private static void appendSourceLocation(StringBuilder json, LogRecord record) {
96107
json.append(SOURCE_LOCATION_KEY).append("{").append(String.join(", ", entries)).append("}, ");
97108
}
98109

110+
private void appendExecutionId(StringBuilder json, LogRecord record) {
111+
json.append("\"execution_id\": \"")
112+
.append(executionIdByThreadMap.get(Integer.toString(record.getThreadID())))
113+
.append("\", ");
114+
}
115+
99116
private static String escapeString(String s) {
100117
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
101118
}
@@ -117,4 +134,12 @@ public void close() throws SecurityException {
117134
out.close();
118135
}
119136
}
137+
138+
public void addExecutionId(long threadId, String executionId) {
139+
executionIdByThreadMap.put(Long.toString(threadId), executionId);
140+
}
141+
142+
public void removeExecutionId(long threadId) {
143+
executionIdByThreadMap.remove(Long.toString(threadId));
144+
}
120145
}

invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.google.common.truth.Expect;
2929
import com.google.gson.Gson;
3030
import com.google.gson.JsonObject;
31+
import com.google.gson.JsonParser;
32+
import com.google.gson.stream.JsonReader;
3133
import io.cloudevents.CloudEvent;
3234
import io.cloudevents.core.builder.CloudEventBuilder;
3335
import io.cloudevents.core.format.EventFormat;
@@ -39,6 +41,7 @@
3941
import java.io.IOException;
4042
import java.io.InputStream;
4143
import java.io.InputStreamReader;
44+
import java.io.StringReader;
4245
import java.io.UncheckedIOException;
4346
import java.net.ServerSocket;
4447
import java.net.URI;
@@ -89,6 +92,8 @@ public class IntegrationTest {
8992
@Rule public final TestName testName = new TestName();
9093

9194
private static final String SERVER_READY_STRING = "Started ServerConnector";
95+
private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID";
96+
private static final String EXECUTION_ID = "1234abcd";
9297

9398
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
9499

@@ -286,14 +291,18 @@ public void exceptionHttp() throws Exception {
286291
String exceptionExpectedOutput =
287292
"\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":"
288293
+ " \"com/google/cloud/functions/invoker/HttpFunctionExecutor.java\", \"method\":"
289-
+ " \"service\"}, \"message\": \"Failed to execute"
294+
+ " \"service\"}, \"execution_id\": \""
295+
+ EXECUTION_ID
296+
+ "\","
297+
+ " \"message\": \"Failed to execute"
290298
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionHttp\\n"
291299
+ "java.lang.RuntimeException: exception thrown for test";
292300
testHttpFunction(
293301
fullTarget("ExceptionHttp"),
294302
ImmutableList.of(
295303
TestCase.builder()
296304
.setExpectedResponseCode(500)
305+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
297306
.setExpectedOutput(exceptionExpectedOutput)
298307
.build()));
299308
}
@@ -303,7 +312,10 @@ public void exceptionBackground() throws Exception {
303312
String exceptionExpectedOutput =
304313
"\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":"
305314
+ " \"com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java\", \"method\":"
306-
+ " \"service\"}, \"message\": \"Failed to execute"
315+
+ " \"service\"}, \"execution_id\": \""
316+
+ EXECUTION_ID
317+
+ "\", "
318+
+ "\"message\": \"Failed to execute"
307319
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionBackground\\n"
308320
+ "java.lang.RuntimeException: exception thrown for test";
309321

@@ -317,6 +329,7 @@ public void exceptionBackground() throws Exception {
317329
ImmutableList.of(
318330
TestCase.builder()
319331
.setRequestText(gcfRequestText)
332+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
320333
.setExpectedResponseCode(500)
321334
.setExpectedOutput(exceptionExpectedOutput)
322335
.build()),
@@ -359,25 +372,37 @@ public void stackDriverLogging() throws Exception {
359372
+ "\"logging.googleapis.com/sourceLocation\": "
360373
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\","
361374
+ " \"method\": \"service\"},"
375+
+ " \"execution_id\": \""
376+
+ EXECUTION_ID
377+
+ "\","
362378
+ " \"message\": \"blim\"}";
363379
TestCase simpleTestCase =
364-
TestCase.builder().setUrl("/?message=blim").setExpectedOutput(simpleExpectedOutput).build();
380+
TestCase.builder()
381+
.setUrl("/?message=blim")
382+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
383+
.setExpectedOutput(simpleExpectedOutput)
384+
.build();
365385
String quotingExpectedOutput = "\"message\": \"foo\\nbar\\\"";
366386
TestCase quotingTestCase =
367387
TestCase.builder()
368388
.setUrl("/?message=" + URLEncoder.encode("foo\nbar\"", "UTF-8"))
389+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
369390
.setExpectedOutput(quotingExpectedOutput)
370391
.build();
371392
String exceptionExpectedOutput =
372393
"{\"severity\": \"ERROR\", "
373394
+ "\"logging.googleapis.com/sourceLocation\": "
374395
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\", "
375396
+ "\"method\": \"service\"}, "
397+
+ "\"execution_id\": \""
398+
+ EXECUTION_ID
399+
+ "\", "
376400
+ "\"message\": \"oops\\njava.lang.Exception: disaster\\n"
377401
+ " at com.google.cloud.functions.invoker.testfunctions.Log.service(Log.java:";
378402
TestCase exceptionTestCase =
379403
TestCase.builder()
380404
.setUrl("/?message=oops&level=severe&exception=disaster")
405+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
381406
.setExpectedOutput(exceptionExpectedOutput)
382407
.build();
383408
testHttpFunction(
@@ -753,7 +778,11 @@ private void testFunction(
753778
for (TestCase testCase : testCases) {
754779
testCase
755780
.expectedOutput()
756-
.ifPresent(output -> expect.that(serverProcess.output()).contains(output));
781+
.ifPresent(
782+
(output) -> {
783+
expect.that(serverProcess.output()).contains(output);
784+
parseLogJson(serverProcess.output());
785+
});
757786
}
758787
// Wait for the output monitor task to terminate. If it threw an exception, we will get an
759788
// ExecutionException here.
@@ -842,7 +871,9 @@ private ServerProcess startServer(
842871
"FUNCTION_SIGNATURE_TYPE",
843872
signatureType.toString(),
844873
"FUNCTION_TARGET",
845-
target);
874+
target,
875+
"LOG_EXECUTION_ID",
876+
"true");
846877
processBuilder.environment().putAll(environment);
847878
processBuilder.environment().putAll(environmentVariables);
848879
Process serverProcess = processBuilder.start();
@@ -879,4 +910,12 @@ private void monitorOutput(
879910
throw new UncheckedIOException(e);
880911
}
881912
}
913+
914+
// Attempt to parse Json object, throws on parse failure
915+
private void parseLogJson(String json) throws RuntimeException {
916+
System.out.println("trying to parse the following object ");
917+
System.out.println(json);
918+
JsonReader reader = new JsonReader(new StringReader(json));
919+
JsonParser.parseReader(reader);
920+
}
882921
}

0 commit comments

Comments
 (0)