Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### September 2, 2026
`2.12.1`
- Emit a structured `runtime_worker_pool_initializing` DEBUG log event once during INIT in multi-concurrent (Lambda Managed Instances) mode, reporting the worker pool size (`workerCount`) and the maximum concurrency the execution environment supports (`executionEnvironmentMaxConcurrency`). Only visible when the function log level is DEBUG or lower; not emitted for standard on-demand functions.

### July 17, 2026
`2.12.0`
- Add `Lambda-Runtime-Invocation-Id` header support for cross-wiring protection. The RIC now echoes the invocation ID received from RAPID on `/next` back on `/response` and `/error`, enabling RAPID to detect and reject stale responses from timed-out invocations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ protected static void startRuntimeLoops(LambdaRequestHandler lambdaRequestHandle
if (concurrencyConfig.isMultiConcurrent()) {
lambdaLogger.log(concurrencyConfig.getConcurrencyConfigMessage(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.INFO : LogLevel.UNDEFINED);
ExecutorService platformThreadExecutor = Executors.newFixedThreadPool(concurrencyConfig.getNumberOfPlatformThreads());
lambdaLogger.logStructuredEvent(
new WorkerPoolInitializedEvent(
concurrencyConfig.getNumberOfPlatformThreads(),
concurrencyConfig.getNumberOfPlatformThreads()),
LogLevel.DEBUG);
try {
for (int i = 0; i < concurrencyConfig.getNumberOfPlatformThreads(); i++) {
startRuntimeLoopWithExecutor(lambdaRequestHandler, lambdaLogger, platformThreadExecutor, runtimeClient);
Expand Down Expand Up @@ -373,4 +378,15 @@ private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exc
protected static URLClassLoader getCustomerClassLoader() {
return customerClassLoader;
}

static class WorkerPoolInitializedEvent {
final String event = "runtime_worker_pool_initializing";
final int workerCount;
final int executionEnvironmentMaxConcurrency;

WorkerPoolInitializedEvent(int workerCount, int executionEnvironmentMaxConcurrency) {
this.workerCount = workerCount;
this.executionEnvironmentMaxConcurrency = executionEnvironmentMaxConcurrency;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public void log(byte[] message) {
this.log(message, LogLevel.UNDEFINED);
}

public void logStructuredEvent(Object event, LogLevel logLevel) {
if (logFiltering.isEnabled(logLevel)) {
this.logMessage(logFormatter.format(event, logLevel), logLevel);
}
}

public void setLambdaContext(LambdaContext lambdaContext) {
this.logFormatter.setLambdaContext(lambdaContext);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ public class JsonLogFormatter implements LogFormatter {

@Override
public String format(String message, LogLevel logLevel) {
return serialize(createLogMessage(message, logLevel));
}

@Override
public String format(Object message, LogLevel logLevel) {
return serialize(createLogMessage(message, logLevel));
}

private String serialize(StructuredLogMessage msg) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
StructuredLogMessage msg = createLogMessage(message, logLevel);
serializer.toJson(msg, stream);
stream.write('\n');
return new String(stream.toByteArray(), StandardCharsets.UTF_8);
}

private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) {
private StructuredLogMessage createLogMessage(Object message, LogLevel logLevel) {
StructuredLogMessage msg = new StructuredLogMessage();
msg.timestamp = dateFormatter.format(LocalDateTime.now());
msg.message = message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
public interface LogFormatter {
String format(String message, LogLevel logLevel);

default String format(Object message, LogLevel logLevel) {
return format(String.valueOf(message), logLevel);
}

default void setLambdaContext(LambdaContext context) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class StructuredLogMessage {
public String timestamp;
public String message;
Comment thread
vip-amzn marked this conversation as resolved.
public Object message;
public LogLevel level;
public String AWSRequestId;
public String tenantId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,63 @@ void testSequentialWithVirtualMachineErrorStopsLoop() throws Throwable {
assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get());
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testWorkerPoolInitializedEventEmittedOnceInMultiConcurrentMode() throws Throwable {
when(concurrencyConfig.isMultiConcurrent()).thenReturn(true);
when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4);

when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger))
.thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException);

AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient);

org.mockito.ArgumentCaptor<Object> eventCaptor = org.mockito.ArgumentCaptor.forClass(Object.class);
verify(lambdaLogger, times(1)).logStructuredEvent(eventCaptor.capture(), eq(LogLevel.DEBUG));

AWSLambda.WorkerPoolInitializedEvent event = (AWSLambda.WorkerPoolInitializedEvent) eventCaptor.getValue();
assertEquals("runtime_worker_pool_initializing", event.event);
assertEquals(4, event.workerCount);
assertEquals(4, event.executionEnvironmentMaxConcurrency);
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testWorkerPoolInitializedEventNotEmittedInSequentialMode() throws Throwable {
when(concurrencyConfig.isMultiConcurrent()).thenReturn(false);

InvocationRequest fatalRequest = mock(InvocationRequest.class);
when(fatalRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn("fatal");
when(runtimeClient.nextInvocation()).thenReturn(fatalRequest);

AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient);

verify(lambdaLogger, never()).logStructuredEvent(any(), any());
}

/*
* Pins the exact wire format of the event through the real JSON formatter (Gson),
* proving the Object-typed StructuredLogMessage.message serializes the event as a
* nested JSON object with exactly the documented schema.
*/
@Test
void testWorkerPoolInitializedEventJsonWireFormat() {
com.amazonaws.services.lambda.runtime.api.client.logging.JsonLogFormatter formatter =
new com.amazonaws.services.lambda.runtime.api.client.logging.JsonLogFormatter();
String output = formatter.format(new AWSLambda.WorkerPoolInitializedEvent(16, 16), LogLevel.DEBUG);

com.amazonaws.lambda.thirdparty.org.json.JSONObject parsed =
new com.amazonaws.lambda.thirdparty.org.json.JSONObject(output);
assertEquals("DEBUG", parsed.getString("level"));
org.junit.jupiter.api.Assertions.assertNotNull(parsed.getString("timestamp"));

com.amazonaws.lambda.thirdparty.org.json.JSONObject message = parsed.getJSONObject("message");
assertEquals("runtime_worker_pool_initializing", message.getString("event"));
assertEquals(16, message.getInt("workerCount"));
assertEquals(16, message.getInt("executionEnvironmentMaxConcurrency"));
assertEquals(3, message.length());
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testInvocationIdIsPassedToReportSuccess() throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,133 @@
package com.amazonaws.services.lambda.runtime.api.client.logging;

import com.amazonaws.lambda.thirdparty.org.json.JSONObject;
import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext;
import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.amazonaws.services.lambda.runtime.logging.LogLevel;

public class JsonLogFormatterTest {

/*
* Gson serialization-neutrality tests for the String -> Object type change of
* StructuredLogMessage.message. These pin the exact serialized behavior so any
* future regression (e.g. from a Gson upgrade changing runtime-type adapter
* resolution) is caught here rather than in customers' log pipelines.
*/

@Test
void testStringMessageStillSerializesAsJsonStringWithObjectDeclaredField() {
JsonLogFormatter formatter = new JsonLogFormatter();
String output = formatter.format("test log", LogLevel.INFO);

JSONObject parsed = new JSONObject(output);
// message must be a JSON string value, not an object or anything else
assertTrue(parsed.get("message") instanceof String);
assertEquals("test log", parsed.getString("message"));
assertEquals("INFO", parsed.getString("level"));
}

@Test
void testStringMessageEscapingUnchangedWithObjectDeclaredField() {
// quotes, backslashes, newlines, unicode and HTML-sensitive chars must
// round-trip exactly as before the field type change
String tricky = "quote\" backslash\\ newline\n tab\t unicode\u00e9 html<>&";
JsonLogFormatter formatter = new JsonLogFormatter();
String output = formatter.format(tricky, LogLevel.WARN);

JSONObject parsed = new JSONObject(output);
assertEquals(tricky, parsed.getString("message"));
}

@Test
void testNullStringMessageOmittedExactlyAsBefore() {
// serializeNulls(false) omitted a null String message before the change;
// a null Object message must behave identically
JsonLogFormatter formatter = new JsonLogFormatter();
String output = formatter.format((String) null, LogLevel.INFO);

JSONObject parsed = new JSONObject(output);
assertFalse(parsed.has("message"));
assertNotNull(parsed.getString("timestamp"));
}

static class SampleStructuredEvent {
final String event = "sample_event";
final int intValue;
final String textValue;

SampleStructuredEvent(int intValue, String textValue) {
this.intValue = intValue;
this.textValue = textValue;
}
}

@Test
void testObjectMessageSerializesAsNestedJsonObject() {
JsonLogFormatter formatter = new JsonLogFormatter();
String output = formatter.format(new SampleStructuredEvent(42, "abc"), LogLevel.DEBUG);

JSONObject parsed = new JSONObject(output);
JSONObject message = parsed.getJSONObject("message");
assertEquals("sample_event", message.getString("event"));
assertEquals(42, message.getInt("intValue"));
assertEquals("abc", message.getString("textValue"));
assertEquals(3, message.length());
assertEquals("DEBUG", parsed.getString("level"));
assertNotNull(parsed.getString("timestamp"));
}

@Test
void testObjectMessageWithLambdaContextKeepsEnvelopeFields() {
JsonLogFormatter formatter = new JsonLogFormatter();
formatter.setLambdaContext(new LambdaContext(
0, 0, "request-id", null, null, "function-name",
null, null, "function-arn", "tenant-id", null, null));
String output = formatter.format(new SampleStructuredEvent(1, "x"), LogLevel.DEBUG);

JSONObject parsed = new JSONObject(output);
assertEquals("request-id", parsed.getString("AWSRequestId"));
assertEquals("tenant-id", parsed.getString("tenantId"));
assertEquals(1, parsed.getJSONObject("message").getInt("intValue"));
}

@Test
void testReflectiveStringAccessToMessageFieldStillWorks() throws Exception {
// StructuredLogMessage is internal, but be conservative about reflective
// consumers: setting and reading a String through the field must not break.
StructuredLogMessage msg = new StructuredLogMessage();
java.lang.reflect.Field field = StructuredLogMessage.class.getDeclaredField("message");
field.set(msg, "reflective string");
assertEquals("reflective string", (String) field.get(msg));

PojoSerializer<StructuredLogMessage> serializer =
GsonFactory.getInstance().getSerializer(StructuredLogMessage.class);
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
serializer.toJson(msg, stream);
JSONObject parsed = new JSONObject(stream.toString("UTF-8"));
assertEquals("reflective string", parsed.getString("message"));
}

@Test
void testStringMessageDeserializationRoundTripUnchanged() {
// fromJson of a string-message log line must still yield a String in the field
JsonLogFormatter formatter = new JsonLogFormatter();
String output = formatter.format("round trip", LogLevel.INFO);

PojoSerializer<StructuredLogMessage> serializer =
GsonFactory.getInstance().getSerializer(StructuredLogMessage.class);
StructuredLogMessage result = serializer.fromJson(output);
assertTrue(result.message instanceof String);
assertEquals("round trip", result.message);
}

@Test
void testFormattingWithoutLambdaContext() {
assertFormatsString("test log", LogLevel.WARN, null);
Expand Down
Loading