diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml index e8344cb8..9dca71fb 100644 --- a/aws-lambda-java-runtime-interface-client/pom.xml +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -53,7 +53,7 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + 1.2.3 com.amazonaws diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java index ade1ea7b..1424de0c 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java @@ -16,6 +16,8 @@ import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClient; import com.amazonaws.services.lambda.runtime.api.client.util.LambdaOutputStream; import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; @@ -187,7 +189,12 @@ public static void main(String[] args) { private static void startRuntime(String handler) { try (LogSink logSink = createLogSink()) { - startRuntime(handler, new LambdaContextLogger(logSink)); + LambdaLogger logger = new LambdaContextLogger( + logSink, + LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), + LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT) + ); + startRuntime(handler, logger); } catch (Throwable t) { throw new Error(t); } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java index 4cd93b53..9e3c48eb 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java @@ -4,6 +4,7 @@ import com.amazonaws.services.lambda.runtime.ClientContext; import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; @@ -11,6 +12,7 @@ import com.amazonaws.services.lambda.runtime.api.client.api.LambdaClientContext; import com.amazonaws.services.lambda.runtime.api.client.api.LambdaCognitoIdentity; import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest; import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; @@ -844,6 +846,22 @@ private void safeAddRequestIdToLog4j(String log4jContextClassName, } } + /** + * Passes the LambdaContext to the logger so that the JSON formatter can include the requestId. + * + * We do casting here because both the LambdaRuntime and the LambdaLogger is in the core package, + * and the setLambdaContext(context) is a method we don't want to publish for customers. That method is + * only implemented on the internal LambdaContextLogger, so we check and cast to be able to call it. + * @param context the LambdaContext + */ + private void safeAddContextToLambdaLogger(LambdaContext context) { + LambdaLogger logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); + if (logger instanceof LambdaContextLogger) { + LambdaContextLogger contextLogger = (LambdaContextLogger) logger; + contextLogger.setLambdaContext(context); + } + } + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { output.reset(); @@ -871,6 +889,8 @@ public ByteArrayOutputStream call(InvocationRequest request) throws Error, Excep clientContext ); + safeAddContextToLambdaLogger(context); + if (LambdaRuntimeInternal.getUseLog4jAppender()) { safeAddRequestIdToLog4j("org.apache.log4j.MDC", request, Object.class); safeAddRequestIdToLog4j("org.apache.logging.log4j.ThreadContext", request, String.class); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java index be509ecb..af357850 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java @@ -12,6 +12,8 @@ public class LambdaEnvironment { public static final int MEMORY_LIMIT = parseInt(ENV_READER.getEnvOrDefault(AWS_LAMBDA_FUNCTION_MEMORY_SIZE, "128")); public static final String LOG_GROUP_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_GROUP_NAME); public static final String LOG_STREAM_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_STREAM_NAME); + public static final String LAMBDA_LOG_LEVEL = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_LEVEL, "UNDEFINED"); + public static final String LAMBDA_LOG_FORMAT = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_FORMAT, "TEXT"); public static final String FUNCTION_NAME = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_NAME); public static final String FUNCTION_VERSION = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_VERSION); } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java index 7a47364a..eca5cef1 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java @@ -66,6 +66,16 @@ public interface ReservedRuntimeEnvironmentVariables { */ String AWS_LAMBDA_LOG_STREAM_NAME = "AWS_LAMBDA_LOG_STREAM_NAME"; + /** + * The logging level set for the function. + */ + String AWS_LAMBDA_LOG_LEVEL = "AWS_LAMBDA_LOG_LEVEL"; + + /** + * The logging format set for the function. + */ + String AWS_LAMBDA_LOG_FORMAT = "AWS_LAMBDA_LOG_FORMAT"; + /** * Access key id obtained from the function's execution role. */ diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java new file mode 100644 index 00000000..b9aa4477 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java @@ -0,0 +1,67 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +/** + * Provides default implementation of the convenience logger functions. + * When extending AbstractLambdaLogger, only one function has to be overridden: + * void logMessage(byte[] message, LogLevel logLevel); + */ +public abstract class AbstractLambdaLogger implements LambdaLogger { + private final LogFiltering logFiltering; + private final LogFormatter logFormatter; + protected final LogFormat logFormat; + + public AbstractLambdaLogger(LogLevel logLevel, LogFormat logFormat) { + this.logFiltering = new LogFiltering(logLevel); + + this.logFormat = logFormat; + if (logFormat == LogFormat.JSON) { + logFormatter = new JsonLogFormatter(); + } else { + logFormatter = new TextLogFormatter(); + } + } + + protected abstract void logMessage(byte[] message, LogLevel logLevel); + + protected void logMessage(String message, LogLevel logLevel) { + logMessage(message.getBytes(UTF_8), logLevel); + } + + @Override + public void log(String message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + this.logMessage(logFormatter.format(message, logLevel), logLevel); + } + } + + @Override + public void log(byte[] message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + // there is no formatting for byte[] messages + this.logMessage(message, logLevel); + } + } + + @Override + public void log(String message) { + this.log(message, LogLevel.UNDEFINED); + } + + @Override + public void log(byte[] message) { + this.log(message, LogLevel.UNDEFINED); + } + + public void setLambdaContext(LambdaContext lambdaContext) { + this.logFormatter.setLambdaContext(lambdaContext); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java index 351a39ed..6663cc1d 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java @@ -2,12 +2,40 @@ package com.amazonaws.services.lambda.runtime.api.client.logging; -public enum FrameType { +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; - LOG(0xa55a0003); +/** + * The first 4 bytes of the framing protocol is the Frame Type, that's made of a magic number (3 bytes) and 1 byte of flags. + * +-----------------------+ + * | Frame Type - 4 bytes | + * +-----------------------+ + * | a5 5a 00 | flgs | + * + - - - - - + - - - - - + + * \ bit | + * | view| + * +---------+ + + * | | + * v byte 3 v F - free + * +-+-+-+-+-+-+-+-+ J - { JsonLog = 0, PlainTextLog = 1 } + * |F|F|F|L|l|l|T|J| T - { NoTimeStamp = 0, TimeStampPresent = 1 } + * +-+-+-+-+-+-+-+-+ Lll -> Log Level in 3-bit binary (L-> most significant bit) + */ +public class FrameType { + private static final int LOG_MAGIC = 0xa55a0000; + private static final int OFFSET_LOG_FORMAT = 0; + private static final int OFFSET_TIMESTAMP_PRESENT = 1; + private static final int OFFSET_LOG_LEVEL = 2; private final int val; + public static int getValue(LogLevel logLevel, LogFormat logFormat) { + return LOG_MAGIC | + (logLevel.ordinal() << OFFSET_LOG_LEVEL) | + (1 << OFFSET_TIMESTAMP_PRESENT) | + (logFormat.ordinal() << OFFSET_LOG_FORMAT); + } + FrameType(int val) { this.val = val; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java index e0ec836d..f20e7722 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java @@ -9,6 +9,9 @@ import java.nio.ByteOrder; import java.time.Instant; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + /** * FramedTelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple * framing protocol so message boundaries can be determined. Each frame can be visualized as follows: @@ -38,16 +41,21 @@ public FramedTelemetryLogSink(FileDescriptor fd) throws IOException { } @Override - public synchronized void log(byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { - writeFrame(message); + writeFrame(logLevel, logFormat, message); } catch (IOException e) { e.printStackTrace(); } } - private void writeFrame(byte[] message) throws IOException { - updateHeader(message.length); + @Override + public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + private void writeFrame(LogLevel logLevel, LogFormat logFormat, byte[] message) throws IOException { + updateHeader(logLevel, logFormat, message.length); this.logOutputStream.write(this.headerBuf.array()); this.logOutputStream.write(message); } @@ -60,9 +68,9 @@ private long timestamp() { /** * Updates the header ByteBuffer with the provided length. The header comprises the frame type and message length. */ - private void updateHeader(int length) { + private void updateHeader(LogLevel logLevel, LogFormat logFormat, int length) { this.headerBuf.clear(); - this.headerBuf.putInt(FrameType.LOG.getValue()); + this.headerBuf.putInt(FrameType.getValue(logLevel, logFormat)); this.headerBuf.putInt(length); this.headerBuf.putLong(timestamp()); this.headerBuf.flip(); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java new file mode 100644 index 00000000..ef9e1c41 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -0,0 +1,55 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class JsonLogFormatter implements LogFormatter { + private final PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + private LambdaContext lambdaContext; + + private static final DateTimeFormatter dateFormatter = + DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .withZone(ZoneId.of("UTC")); + + @Override + public String format(String message, LogLevel logLevel) { + 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) { + StructuredLogMessage msg = new StructuredLogMessage(); + msg.timestamp = dateFormatter.format(LocalDateTime.now()); + msg.message = message; + msg.level = logLevel; + + if (lambdaContext != null) { + msg.AWSRequestId = lambdaContext.getAwsRequestId(); + } + return msg; + } + + + /** + * Function to set the context for every invocation. + * This way the logger will be able to attach additional information to the log packet. + */ + @Override + public void setLambdaContext(LambdaContext context) { + this.lambdaContext = context; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java index eaa2e3ea..4800b356 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java @@ -2,33 +2,29 @@ package com.amazonaws.services.lambda.runtime.api.client.logging; -import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import static java.nio.charset.StandardCharsets.UTF_8; -public class LambdaContextLogger implements LambdaLogger { +public class LambdaContextLogger extends AbstractLambdaLogger { // If a null string is passed in, replace it with "null", // replicating the behavior of System.out.println(null); private static final byte[] NULL_BYTES_VALUE = "null".getBytes(UTF_8); private final transient LogSink sink; - public LambdaContextLogger(LogSink sink) { + public LambdaContextLogger(LogSink sink, LogLevel logLevel, LogFormat logFormat) { + super(logLevel, logFormat); this.sink = sink; } - public void log(byte[] message) { + @Override + protected void logMessage(byte[] message, LogLevel logLevel) { if (message == null) { message = NULL_BYTES_VALUE; } - sink.log(message); - } - - public void log(String message) { - if (message == null) { - this.log(NULL_BYTES_VALUE); - } else { - this.log(message.getBytes(UTF_8)); - } + sink.log(logLevel, this.logFormat, message); } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java new file mode 100644 index 00000000..59038efa --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java @@ -0,0 +1,17 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class LogFiltering { + private final LogLevel minimumLogLevel; + + public LogFiltering(LogLevel minimumLogLevel) { + this.minimumLogLevel = minimumLogLevel; + } + + boolean isEnabled(LogLevel logLevel) { + return (logLevel == LogLevel.UNDEFINED || logLevel.ordinal() >= minimumLogLevel.ordinal()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java new file mode 100644 index 00000000..debe5af4 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java @@ -0,0 +1,13 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public interface LogFormatter { + String format(String message, LogLevel logLevel); + + default void setLambdaContext(LambdaContext context) { + }; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java index 11c3f92c..77df08e2 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java @@ -4,8 +4,13 @@ import java.io.Closeable; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + public interface LogSink extends Closeable { void log(byte[] message); + void log(LogLevel logLevel, LogFormat logFormat, byte[] message); + } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java index f36c4130..6fd6b87c 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java @@ -4,9 +4,16 @@ import java.io.IOException; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + public class StdOutLogSink implements LogSink { @Override public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + public void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { System.out.write(message); } catch (IOException e) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java new file mode 100644 index 00000000..9c271adf --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java @@ -0,0 +1,12 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class StructuredLogMessage { + public String timestamp; + public String message; + public LogLevel level; + public String AWSRequestId; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java new file mode 100644 index 00000000..7c345401 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java @@ -0,0 +1,28 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +import java.util.HashMap; + +public class TextLogFormatter implements LogFormatter { + private static final HashMap logLevelMapper = new HashMap() {{ + for (LogLevel logLevel: LogLevel.values()) { + put(logLevel, "[" + logLevel.toString() + "] "); + } + }}; + + @Override + public String format(String message, LogLevel logLevel) { + if (logLevel == LogLevel.UNDEFINED) { + return message; + } + + return new StringBuilder() + .append(logLevelMapper.get(logLevel)) + .append(message) + .toString(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java new file mode 100644 index 00000000..dca307c9 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java @@ -0,0 +1,74 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import org.junit.jupiter.api.Test; + +import java.util.LinkedList; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + + +public class AbstractLambdaLoggerTest { + class TestLogger extends AbstractLambdaLogger { + private List messages = new LinkedList<>(); + + public TestLogger(LogLevel logLevel, LogFormat logFormat) { + super(logLevel, logFormat); + } + + @Override + protected void logMessage(byte[] message, LogLevel logLevel) { + messages.add(message); + } + + List getMessages() { + return messages; + } + } + + private void logMessages(LambdaLogger logger) { + logger.log("trace", LogLevel.TRACE); + logger.log("debug", LogLevel.DEBUG); + logger.log("info", LogLevel.INFO); + logger.log("warn", LogLevel.WARN); + logger.log("error", LogLevel.ERROR); + logger.log("fatal", LogLevel.FATAL); + } + + @Test + public void testWithoutFiltering() { + TestLogger logger = new TestLogger(LogLevel.UNDEFINED, LogFormat.TEXT); + logMessages(logger); + + assertEquals(6, logger.getMessages().size()); + } + + @Test + public void testWithFiltering() { + TestLogger logger = new TestLogger(LogLevel.WARN, LogFormat.TEXT); + logMessages(logger); + + assertEquals(3, logger.getMessages().size()); + } + + @Test + public void testUndefinedLogLevelWithFiltering() { + TestLogger logger = new TestLogger(LogLevel.WARN, LogFormat.TEXT); + logger.log("undefined"); + + assertEquals(1, logger.getMessages().size()); + } + + @Test + public void testFormattingLogMessages() { + TestLogger logger = new TestLogger(LogLevel.INFO, LogFormat.TEXT); + logger.log("test message", LogLevel.INFO); + + assertEquals(1, logger.getMessages().size()); + assertEquals("[INFO] test message", new String(logger.getMessages().get(0))); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java new file mode 100644 index 00000000..65078790 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java @@ -0,0 +1,39 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + +public class FrameTypeTest { + + @Test + public void logFrames() { + assertHexEquals( + 0xa55a0003, + FrameType.getValue(LogLevel.UNDEFINED, LogFormat.TEXT) + ); + + assertHexEquals( + 0xa55a001b, + FrameType.getValue(LogLevel.FATAL, LogFormat.TEXT) + ); + } + + + /** + * Helper function to make it easier to debug failing test. + * + * @param expected Expected value as int + * @param actual Actual value as int + */ + private void assertHexEquals(int expected, int actual) { + assertEquals( + Integer.toHexString(expected), + Integer.toHexString(actual) + ); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java index e8dbb73b..e3e68a69 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java @@ -20,6 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + public class FramedTelemetryLogSinkTest { private static final int DEFAULT_BUFFER_SIZE = 256; @@ -35,13 +38,16 @@ private long timestamp() { @Test public void logSingleFrame() throws IOException { - byte[] message = "hello world\nsomething on a new line!\n".getBytes(); + byte[] message = "{\"message\": \"hello world\nsomething on a new line!\"}".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.JSON; + File tmpFile = tmpFolder.resolve("pipe").toFile(); FileOutputStream fos = new FileOutputStream(tmpFile); FileDescriptor fd = fos.getFD(); long before = timestamp(); try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { - logSink.log(message); + logSink.log(logLevel, logFormat, message); } long after = timestamp(); @@ -54,7 +60,7 @@ public void logSingleFrame() throws IOException { // first 4 bytes indicate the type int type = buf.getInt(); - assertEquals(FrameType.LOG.getValue(), type); + assertEquals(FrameType.getValue(logLevel, logFormat), type); // next 4 bytes indicate the length of the message int len = buf.getInt(); @@ -71,7 +77,7 @@ public void logSingleFrame() throws IOException { assertArrayEquals(message, actual); // rest of buffer should be empty - while(buf.hasRemaining()) + while (buf.hasRemaining()) assertEquals(ZERO_BYTE, buf.get()); } @@ -79,13 +85,16 @@ public void logSingleFrame() throws IOException { public void logMultipleFrames() throws IOException { byte[] firstMessage = "hello world\nsomething on a new line!".getBytes(); byte[] secondMessage = "hello again\nhere's another message\n".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.TEXT; + File tmpFile = tmpFolder.resolve("pipe").toFile(); FileOutputStream fos = new FileOutputStream(tmpFile); FileDescriptor fd = fos.getFD(); long before = timestamp(); try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { - logSink.log(firstMessage); - logSink.log(secondMessage); + logSink.log(logLevel, logFormat, firstMessage); + logSink.log(logLevel, logFormat, secondMessage); } long after = timestamp(); @@ -96,10 +105,10 @@ public void logMultipleFrames() throws IOException { // reset the position to the start buf.position(0); - for(byte[] message : Arrays.asList(firstMessage, secondMessage)) { + for (byte[] message : Arrays.asList(firstMessage, secondMessage)) { // first 4 bytes indicate the type int type = buf.getInt(); - assertEquals(FrameType.LOG.getValue(), type); + assertEquals(FrameType.getValue(logLevel, logFormat), type); // next 4 bytes indicate the length of the message int len = buf.getInt(); @@ -117,7 +126,7 @@ public void logMultipleFrames() throws IOException { } // rest of buffer should be empty - while(buf.hasRemaining()) + while (buf.hasRemaining()) assertEquals(ZERO_BYTE, buf.get()); } @@ -125,7 +134,7 @@ public void logMultipleFrames() throws IOException { * The implementation of FramedTelemetryLogSink was based on java.nio.channels.WritableByteChannel which would * throw ClosedByInterruptException if Thread.currentThread.interrupt() was called. The implementation was changed * and this test ensures that logging works even if the current thread was interrupted. - * + *

* https://t.corp.amazon.com/0304370986/ */ @Test @@ -138,7 +147,7 @@ public void interruptedThread() throws IOException { try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { Thread.currentThread().interrupt(); - logSink.log(message); + logSink.log(LogLevel.ERROR, LogFormat.TEXT, message); } byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; @@ -150,8 +159,8 @@ public void interruptedThread() throws IOException { assertEquals(expectedBytes, readBytes); - for(int i = 0; i < message.length; i++) { - assertEquals(buffer[i + headerSizeBytes], message[i]); + for (int i = 0; i < message.length; i++) { + assertEquals(message[i], buffer[i + headerSizeBytes]); } } finally { // clear interrupted status of the current thread diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java new file mode 100644 index 00000000..8630d5fe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -0,0 +1,57 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +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.assertNotNull; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class JsonLogFormatterTest { + + @Test + void testFormattingWithoutLambdaContext() { + assertFormatsString("test log", LogLevel.WARN, null); + } + + @Test + void testFormattingWithLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + void assertFormatsString(String message, LogLevel logLevel, LambdaContext context) { + JsonLogFormatter logFormatter = new JsonLogFormatter(); + if (context != null) { + logFormatter.setLambdaContext(context); + } + String output = logFormatter.format(message, logLevel); + + PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + assert_expected_log_message(serializer.fromJson(output), message, logLevel, context); + } + + void assert_expected_log_message(StructuredLogMessage result, String message, LogLevel logLevel, LambdaContext context) { + assertEquals(message, result.message); + assertEquals(logLevel, result.level); + assertNotNull(result.timestamp); + + if (context != null) { + assertEquals(context.getAwsRequestId(), result.AWSRequestId); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java index 83399646..b1bbefc4 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java @@ -10,6 +10,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + public class StdOutLogSinkTest { private final PrintStream originalOutPrintStream = System.out; @@ -35,6 +38,20 @@ public void testSingleLog() { assertEquals("hello\nworld", bos.toString()); } + @Test + public void testSingleLogWithLogLevel() { + System.setOut(capturedOutPrintStream); + try { + try (StdOutLogSink logSink = new StdOutLogSink()) { + logSink.log(LogLevel.ERROR, LogFormat.TEXT, "hello\nworld".getBytes()); + } + } finally { + System.setOut(originalOutPrintStream); + } + + assertEquals("hello\nworld", bos.toString()); + } + @Test public void testContextLoggerWithStdoutLogSink_logBytes() { System.setOut(capturedOutPrintStream); diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java new file mode 100644 index 00000000..598074a3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java @@ -0,0 +1,25 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class TextLogFormatterTest { + @Test + void testFormattingStringWithLogLevel() { + assertFormatsString("test log", LogLevel.WARN, "[WARN] test log"); + } + + @Test + void testFormattingStringWithoutLogLevel() { + assertFormatsString("test log", LogLevel.UNDEFINED, "test log"); + } + + void assertFormatsString(String input, LogLevel logLevel, String expected) { + LogFormatter logFormatter = new TextLogFormatter(); + String output = logFormatter.format(input, logLevel); + assertEquals(expected, output); + } +} \ No newline at end of file