Skip to content

Commit 30c88e8

Browse files
committed
feat: Add execution id logging to uniquely identify request logs
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 36a1566 commit 30c88e8

File tree

6 files changed

+112
-4
lines changed

6 files changed

+112
-4
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.cloud.functions.BackgroundFunction;
2121
import com.google.cloud.functions.CloudEventsFunction;
2222
import com.google.cloud.functions.Context;
23+
import com.google.cloud.functions.invoker.gcf.ExecutionIdUtil;
2324
import com.google.cloud.functions.RawBackgroundFunction;
2425
import com.google.gson.Gson;
2526
import com.google.gson.GsonBuilder;
@@ -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;
@@ -322,6 +324,7 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
322324
@Override
323325
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
324326
String contentType = req.getContentType();
327+
executionIdUtil.storeExecutionId(req);
325328
try {
326329
if ((contentType != null && contentType.startsWith("application/cloudevents+json"))
327330
|| req.getHeader("ce-specversion") != null) {
@@ -334,6 +337,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) throws IOEx
334337
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
335338
logger.log(Level.SEVERE, "Failed to execute " + functionExecutor.functionName(), t);
336339
}
340+
executionIdUtil.removeExecutionId();
337341
}
338342

339343
private enum CloudEventKind {
@@ -348,6 +352,7 @@ private enum CloudEventKind {
348352
* FunctionExecutor}.
349353
*/
350354
private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exception {
355+
executionIdUtil.storeExecutionId(req);
351356
@SuppressWarnings("unchecked")
352357
FunctionExecutor<CloudEventT> executor = (FunctionExecutor<CloudEventT>) functionExecutor;
353358
byte[] body = req.getInputStream().readAllBytes();
@@ -362,6 +367,7 @@ private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exce
362367
runWithContextClassLoader(() -> executor.serviceCloudEvent(reader.toEvent(data -> data)));
363368
// The data->data is a workaround for a bug fixed since Milestone 4 of the SDK, in
364369
// https://github.com/cloudevents/sdk-java/pull/259.
370+
executionIdUtil.removeExecutionId();
365371
}
366372

367373
private static Map<String, List<String>> headerMap(HttpServletRequest req) {

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;
@@ -64,6 +66,7 @@ public static HttpFunctionExecutor forClass(Class<?> functionClass) {
6466
/** Executes the user's method, can handle all HTTP type methods. */
6567
@Override
6668
public void service(HttpServletRequest req, HttpServletResponse res) {
69+
executionIdUtil.storeExecutionId(req);
6770
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
6871
HttpResponseImpl respImpl = new HttpResponseImpl(res);
6972
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
@@ -77,5 +80,6 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
7780
Thread.currentThread().setContextClassLoader(oldContextLoader);
7881
respImpl.flush();
7982
}
83+
executionIdUtil.removeExecutionId();
8084
}
8185
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.google.cloud.functions.invoker.gcf;
2+
3+
import javax.servlet.http.HttpServletRequest;
4+
import java.util.logging.Handler;
5+
import java.util.logging.Logger;
6+
import java.util.Random;
7+
import java.util.Base64;
8+
9+
/**
10+
* A helper class that fetches either fetches a unique execution id from request HTTP headers or
11+
* generates a random id.
12+
*/
13+
public final class ExecutionIdUtil {
14+
private static final Logger rootLogger = Logger.getLogger("");
15+
private static final int EXECUTION_ID_LENGTH = 12;
16+
private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID";
17+
private static final String LOG_EXECUTION_ID_ENV_NAME = "LOG_EXECUTION_ID";
18+
19+
private final Random random = new Random();
20+
21+
/**
22+
* Add mapping to root logger from current thread id to execution id.
23+
* This mapping will be used to append the execution id to log lines.
24+
*/
25+
public void storeExecutionId(HttpServletRequest request) {
26+
if (!executionIdLoggingEnabled()) {
27+
return;
28+
}
29+
for (Handler handler : rootLogger.getHandlers()) {
30+
if (handler instanceof JsonLogHandler) {
31+
String id = getOrGenerateExecutionId(request);
32+
((JsonLogHandler)handler).addExecutionId(Thread.currentThread().getId(), id);
33+
}
34+
}
35+
}
36+
37+
/** Remove mapping from curent thread to request execution id */
38+
public void removeExecutionId() {
39+
if (!executionIdLoggingEnabled()) {
40+
return;
41+
}
42+
for (Handler handler : rootLogger.getHandlers()) {
43+
if (handler instanceof JsonLogHandler) {
44+
((JsonLogHandler)handler).removeExecutionId(Thread.currentThread().getId());
45+
}
46+
}
47+
}
48+
49+
private String getOrGenerateExecutionId(HttpServletRequest request) {
50+
String executionId = request.getHeader(EXECUTION_ID_HTTP_HEADER);
51+
if (executionId == null) {
52+
byte[] array = new byte[EXECUTION_ID_LENGTH];
53+
random.nextBytes(array);
54+
executionId = Base64.getEncoder().encodeToString(array);
55+
}
56+
return executionId;
57+
}
58+
59+
private boolean executionIdLoggingEnabled() {
60+
return Boolean.parseBoolean(System.getenv().getOrDefault(LOG_EXECUTION_ID_ENV_NAME, "false"));
61+
}
62+
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import java.util.logging.Handler;
99
import java.util.logging.Level;
1010
import java.util.logging.LogRecord;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.ConcurrentMap;
1113

1214
/**
1315
* A log handler that publishes log messages in a json format. This is StackDriver's <a
@@ -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 final ConcurrentMap<String, String> executionIdByThreadMap = new ConcurrentHashMap<>();
36+
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,10 @@ 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\": \"").append(executionIdByThreadMap.get(Integer.toString(record.getThreadID()))).append("\", ");
112+
}
113+
99114
private static String escapeString(String s) {
100115
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
101116
}
@@ -117,4 +132,12 @@ public void close() throws SecurityException {
117132
out.close();
118133
}
119134
}
135+
136+
public void addExecutionId(long threadId, String executionId) {
137+
executionIdByThreadMap.put(Long.toString(threadId), executionId);
138+
}
139+
140+
public void removeExecutionId(long threadId) {
141+
executionIdByThreadMap.remove(Long.toString(threadId));
142+
}
120143
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public class IntegrationTest {
8989
@Rule public final TestName testName = new TestName();
9090

9191
private static final String SERVER_READY_STRING = "Started ServerConnector";
92+
private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID";
93+
private static final String EXECUTION_ID = "1234abcd";
9294

9395
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
9496

@@ -286,14 +288,16 @@ public void exceptionHttp() throws Exception {
286288
String exceptionExpectedOutput =
287289
"\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":"
288290
+ " \"com/google/cloud/functions/invoker/HttpFunctionExecutor.java\", \"method\":"
289-
+ " \"service\"}, \"message\": \"Failed to execute"
291+
+ " \"service\"}, \"execution_id\": \"" + EXECUTION_ID + "\","
292+
+ " \"message\": \"Failed to execute"
290293
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionHttp\\n"
291294
+ "java.lang.RuntimeException: exception thrown for test";
292295
testHttpFunction(
293296
fullTarget("ExceptionHttp"),
294297
ImmutableList.of(
295298
TestCase.builder()
296299
.setExpectedResponseCode(500)
300+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
297301
.setExpectedOutput(exceptionExpectedOutput)
298302
.build()));
299303
}
@@ -303,7 +307,8 @@ public void exceptionBackground() throws Exception {
303307
String exceptionExpectedOutput =
304308
"\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":"
305309
+ " \"com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java\", \"method\":"
306-
+ " \"service\"}, \"message\": \"Failed to execute"
310+
+ " \"service\"}, \"execution_id\": \"" + EXECUTION_ID + "\", "
311+
+ "\"message\": \"Failed to execute"
307312
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionBackground\\n"
308313
+ "java.lang.RuntimeException: exception thrown for test";
309314

@@ -317,6 +322,7 @@ public void exceptionBackground() throws Exception {
317322
ImmutableList.of(
318323
TestCase.builder()
319324
.setRequestText(gcfRequestText)
325+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
320326
.setExpectedResponseCode(500)
321327
.setExpectedOutput(exceptionExpectedOutput)
322328
.build()),
@@ -359,25 +365,29 @@ public void stackDriverLogging() throws Exception {
359365
+ "\"logging.googleapis.com/sourceLocation\": "
360366
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\","
361367
+ " \"method\": \"service\"},"
368+
+ " \"execution_id\": \"" + EXECUTION_ID + "\","
362369
+ " \"message\": \"blim\"}";
363370
TestCase simpleTestCase =
364-
TestCase.builder().setUrl("/?message=blim").setExpectedOutput(simpleExpectedOutput).build();
371+
TestCase.builder().setUrl("/?message=blim").setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID)).setExpectedOutput(simpleExpectedOutput).build();
365372
String quotingExpectedOutput = "\"message\": \"foo\\nbar\\\"";
366373
TestCase quotingTestCase =
367374
TestCase.builder()
368375
.setUrl("/?message=" + URLEncoder.encode("foo\nbar\"", "UTF-8"))
376+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
369377
.setExpectedOutput(quotingExpectedOutput)
370378
.build();
371379
String exceptionExpectedOutput =
372380
"{\"severity\": \"ERROR\", "
373381
+ "\"logging.googleapis.com/sourceLocation\": "
374382
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\", "
375383
+ "\"method\": \"service\"}, "
384+
+ "\"execution_id\": \"" + EXECUTION_ID + "\", "
376385
+ "\"message\": \"oops\\njava.lang.Exception: disaster\\n"
377386
+ " at com.google.cloud.functions.invoker.testfunctions.Log.service(Log.java:";
378387
TestCase exceptionTestCase =
379388
TestCase.builder()
380389
.setUrl("/?message=oops&level=severe&exception=disaster")
390+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
381391
.setExpectedOutput(exceptionExpectedOutput)
382392
.build();
383393
testHttpFunction(
@@ -842,7 +852,9 @@ private ServerProcess startServer(
842852
"FUNCTION_SIGNATURE_TYPE",
843853
signatureType.toString(),
844854
"FUNCTION_TARGET",
845-
target);
855+
target,
856+
"LOG_EXECUTION_ID",
857+
"true");
846858
processBuilder.environment().putAll(environment);
847859
processBuilder.environment().putAll(environmentVariables);
848860
Process serverProcess = processBuilder.start();

0 commit comments

Comments
 (0)