Skip to content

Commit 17e84cf

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 17e84cf

File tree

6 files changed

+125
-4
lines changed

6 files changed

+125
-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
@@ -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;
@@ -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 java.util.Base64;
4+
import java.util.Random;
5+
import java.util.logging.Handler;
6+
import java.util.logging.Logger;
7+
import javax.servlet.http.HttpServletRequest;
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. This mapping will be used to
23+
* 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: 24 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,13 @@ 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<>();
2736

2837
public JsonLogHandler(PrintStream out, boolean closePrintStreamOnClose) {
2938
this.out = out;
@@ -38,6 +47,7 @@ public void publish(LogRecord record) {
3847
StringBuilder json = new StringBuilder("{");
3948
appendSeverity(json, record);
4049
appendSourceLocation(json, record);
50+
appendExecutionId(json, record);
4151
appendMessage(json, record); // must be last, see appendMessage
4252
json.append("}");
4353
// We must output the log all at once (should only call println once per call to publish)
@@ -96,6 +106,12 @@ private static void appendSourceLocation(StringBuilder json, LogRecord record) {
96106
json.append(SOURCE_LOCATION_KEY).append("{").append(String.join(", ", entries)).append("}, ");
97107
}
98108

109+
private void appendExecutionId(StringBuilder json, LogRecord record) {
110+
json.append("\"execution_id\": \"")
111+
.append(executionIdByThreadMap.get(Integer.toString(record.getThreadID())))
112+
.append("\", ");
113+
}
114+
99115
private static String escapeString(String s) {
100116
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
101117
}
@@ -117,4 +133,12 @@ public void close() throws SecurityException {
117133
out.close();
118134
}
119135
}
136+
137+
public void addExecutionId(long threadId, String executionId) {
138+
executionIdByThreadMap.put(Long.toString(threadId), executionId);
139+
}
140+
141+
public void removeExecutionId(long threadId) {
142+
executionIdByThreadMap.remove(Long.toString(threadId));
143+
}
120144
}

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

Lines changed: 28 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,18 @@ 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\": \""
292+
+ EXECUTION_ID
293+
+ "\","
294+
+ " \"message\": \"Failed to execute"
290295
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionHttp\\n"
291296
+ "java.lang.RuntimeException: exception thrown for test";
292297
testHttpFunction(
293298
fullTarget("ExceptionHttp"),
294299
ImmutableList.of(
295300
TestCase.builder()
296301
.setExpectedResponseCode(500)
302+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
297303
.setExpectedOutput(exceptionExpectedOutput)
298304
.build()));
299305
}
@@ -303,7 +309,10 @@ public void exceptionBackground() throws Exception {
303309
String exceptionExpectedOutput =
304310
"\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":"
305311
+ " \"com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java\", \"method\":"
306-
+ " \"service\"}, \"message\": \"Failed to execute"
312+
+ " \"service\"}, \"execution_id\": \""
313+
+ EXECUTION_ID
314+
+ "\", "
315+
+ "\"message\": \"Failed to execute"
307316
+ " com.google.cloud.functions.invoker.testfunctions.ExceptionBackground\\n"
308317
+ "java.lang.RuntimeException: exception thrown for test";
309318

@@ -317,6 +326,7 @@ public void exceptionBackground() throws Exception {
317326
ImmutableList.of(
318327
TestCase.builder()
319328
.setRequestText(gcfRequestText)
329+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
320330
.setExpectedResponseCode(500)
321331
.setExpectedOutput(exceptionExpectedOutput)
322332
.build()),
@@ -359,25 +369,37 @@ public void stackDriverLogging() throws Exception {
359369
+ "\"logging.googleapis.com/sourceLocation\": "
360370
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\","
361371
+ " \"method\": \"service\"},"
372+
+ " \"execution_id\": \""
373+
+ EXECUTION_ID
374+
+ "\","
362375
+ " \"message\": \"blim\"}";
363376
TestCase simpleTestCase =
364-
TestCase.builder().setUrl("/?message=blim").setExpectedOutput(simpleExpectedOutput).build();
377+
TestCase.builder()
378+
.setUrl("/?message=blim")
379+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
380+
.setExpectedOutput(simpleExpectedOutput)
381+
.build();
365382
String quotingExpectedOutput = "\"message\": \"foo\\nbar\\\"";
366383
TestCase quotingTestCase =
367384
TestCase.builder()
368385
.setUrl("/?message=" + URLEncoder.encode("foo\nbar\"", "UTF-8"))
386+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
369387
.setExpectedOutput(quotingExpectedOutput)
370388
.build();
371389
String exceptionExpectedOutput =
372390
"{\"severity\": \"ERROR\", "
373391
+ "\"logging.googleapis.com/sourceLocation\": "
374392
+ "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\", "
375393
+ "\"method\": \"service\"}, "
394+
+ "\"execution_id\": \""
395+
+ EXECUTION_ID
396+
+ "\", "
376397
+ "\"message\": \"oops\\njava.lang.Exception: disaster\\n"
377398
+ " at com.google.cloud.functions.invoker.testfunctions.Log.service(Log.java:";
378399
TestCase exceptionTestCase =
379400
TestCase.builder()
380401
.setUrl("/?message=oops&level=severe&exception=disaster")
402+
.setHttpHeaders(ImmutableMap.of(EXECUTION_ID_HTTP_HEADER, EXECUTION_ID))
381403
.setExpectedOutput(exceptionExpectedOutput)
382404
.build();
383405
testHttpFunction(
@@ -842,7 +864,9 @@ private ServerProcess startServer(
842864
"FUNCTION_SIGNATURE_TYPE",
843865
signatureType.toString(),
844866
"FUNCTION_TARGET",
845-
target);
867+
target,
868+
"LOG_EXECUTION_ID",
869+
"true");
846870
processBuilder.environment().putAll(environment);
847871
processBuilder.environment().putAll(environmentVariables);
848872
Process serverProcess = processBuilder.start();

0 commit comments

Comments
 (0)