From db6f8b7cab2a5c1b30e6cde15ff765d8c7c38104 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Tue, 22 Nov 2022 18:06:30 +0100 Subject: [PATCH] logback implementation of the logging module --- .../LambdaJsonLayout.java | 137 ++++++++++++++ powertools-logging-logback/pom.xml | 113 ++++++++++++ .../powertools/logging/LambdaEcsEncoder.java | 94 ++++++++++ .../powertools/logging/LambdaJsonEncoder.java | 86 +++++++++ .../logging/internal/JsonUtils.java | 90 +++++++++ .../logging/internal/LambdaEcsSerializer.java | 171 ++++++++++++++++++ .../internal/LambdaJsonSerializer.java | 96 ++++++++++ .../internal/LogbackLoggingManager.java | 35 ++++ .../internal/LogbackLoggingManagerTest.java | 37 ++++ .../src/test/resources/logback-test.xml | 13 ++ 10 files changed, 872 insertions(+) create mode 100644 powertools-logging-logback/LambdaJsonLayout.java create mode 100644 powertools-logging-logback/pom.xml create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaEcsEncoder.java create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaJsonEncoder.java create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonUtils.java create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsSerializer.java create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonSerializer.java create mode 100644 powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManager.java create mode 100644 powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManagerTest.java create mode 100644 powertools-logging-logback/src/test/resources/logback-test.xml diff --git a/powertools-logging-logback/LambdaJsonLayout.java b/powertools-logging-logback/LambdaJsonLayout.java new file mode 100644 index 000000000..7c9b59ad7 --- /dev/null +++ b/powertools-logging-logback/LambdaJsonLayout.java @@ -0,0 +1,137 @@ +package software.amazon.lambda.powertools.logging; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.LayoutBase; + +/** + * Custom layout for logback that encodes logs in JSON format. + * It does not use a JSON library but a custom serializer ({@link LambdaJsonSerializer}) to reduce the weight of the library. + */ +public class LambdaJsonLayout extends LayoutBase { + private final static String CONTENT_TYPE = "application/json"; + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + private ThrowableHandlingConverter throwableConverter; + private String timestampFormat; + private String timestampFormatTimezoneId; + private boolean includeThreadInfo; + + @Override + public String doLayout(ILoggingEvent event) { + StringBuilder builder = new StringBuilder(256); + LambdaJsonSerializer.serializeObjectStart(builder); + LambdaJsonSerializer.serializeLogLevel(builder, event.getLevel()); + LambdaJsonSerializer.serializeFormattedMessage(builder, event.getFormattedMessage()); + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableConverter.convert(event), throwableProxy.getStackTraceElementProxyArray()[0].toString()); + } else if (throwableProxy instanceof ThrowableProxy) { + LambdaJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); + } else { + LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert(event), throwableProxy.getStackTraceElementProxyArray()[0].toString()); + } + } + LambdaJsonSerializer.serializePowertools(builder, event.getMDCPropertyMap()); + if (includeThreadInfo) { + LambdaJsonSerializer.serializeThreadName(builder, event.getThreadName()); + LambdaJsonSerializer.serializeThreadId(builder, String.valueOf(Thread.currentThread().getId())); + LambdaJsonSerializer.serializeThreadPriority(builder, String.valueOf(Thread.currentThread().getPriority())); + } + LambdaJsonSerializer.serializeTimestamp(builder, event.getTimeStamp(), timestampFormat, timestampFormatTimezoneId); + LambdaJsonSerializer.serializeObjectEnd(builder); + return builder.toString(); + } + + @Override + public String getContentType() { + return CONTENT_TYPE; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + +// public static final String INSTANT_ATTR_NAME = "instant"; +// public static final String EPOCH_SEC_ATTR_NAME = "epochSecond"; +// public static final String NANO_SEC_ATTR_NAME = "nanoOfSecond"; +// public static final String LOGGER_FQCN_ATTR_NAME = "loggerFqcn"; +// public static final String LOGGER_ATTR_NAME = "loggerName"; +// public static final String THREAD_ID_ATTR_NAME = "threadId"; +// public static final String THREAD_PRIORITY_ATTR_NAME = "threadPriority"; +// +// private boolean includePowertools; +// private boolean includeInstant; +// private boolean includeThreadInfo; +// +// public LambdaJsonLayout() { +// super(); +// this.includeInstant = true; +// this.includePowertools = true; +// this.includeThreadInfo = true; +// } +// +// @Override +// protected Map toJsonMap(ILoggingEvent event) { +// Map map = new LinkedHashMap<>(); +// addTimestamp(TIMESTAMP_ATTR_NAME, this.includeTimestamp, event.getTimeStamp(), map); +// addInstant(this.includeInstant, event.getTimeStamp(), event.getNanoseconds(), map); +// add(THREAD_ATTR_NAME, this.includeThreadName || this.includeThreadInfo, event.getThreadName(), map); +// add(LEVEL_ATTR_NAME, this.includeLevel, String.valueOf(event.getLevel()), map); +// add(LOGGER_ATTR_NAME, this.includeLoggerName, event.getLoggerName(), map); +// add(FORMATTED_MESSAGE_ATTR_NAME, this.includeFormattedMessage, event.getFormattedMessage(), map); +// addThrowableInfo(EXCEPTION_ATTR_NAME, this.includeException, event, map); +// // contextStack ? +// // endOfBatch ? +// map.put(LOGGER_FQCN_ATTR_NAME, "ch.qos.logback.classic.Logger"); +// add(THREAD_ID_ATTR_NAME, this.includeThreadInfo, String.valueOf(Thread.currentThread().getId()), map); +// add(THREAD_PRIORITY_ATTR_NAME, this.includeThreadInfo, String.valueOf(Thread.currentThread().getPriority()), map); +// addPowertools(this.includePowertools, event.getMDCPropertyMap(), map); +// return map; +// } +// +// private void addPowertools(boolean includePowertools, Map mdcPropertyMap, Map map) { +// TreeMap sortedMap = new TreeMap<>(mdcPropertyMap); +// List powertoolsFields = DefaultLambdaFields.stringValues(); +// +// sortedMap.forEach((k, v) -> { +// if (includePowertools || !powertoolsFields.contains(k)) { +// map.put(k, v); +// } +// }); +// +// } +// +// private void addInstant(boolean includeInstant, long timeStamp, int nanoseconds, Map map) { +// if (includeInstant) { +// Map instantMap = new LinkedHashMap<>(); +// instantMap.put(EPOCH_SEC_ATTR_NAME, timeStamp / 1000); +// instantMap.put(NANO_SEC_ATTR_NAME, nanoseconds); +// map.put(LambdaJsonLayout.INSTANT_ATTR_NAME, instantMap); +// } +// } +// +// public void setIncludeInstant(boolean includeInstant) { +// this.includeInstant = includeInstant; +// } +// +// public void setIncludePowertools(boolean includePowertools) { +// this.includePowertools = includePowertools; +// } +// + public void setIncludeThreadInfo(boolean includeThreadInfo) { + this.includeThreadInfo = includeThreadInfo; + } + +} diff --git a/powertools-logging-logback/pom.xml b/powertools-logging-logback/pom.xml new file mode 100644 index 000000000..94358f549 --- /dev/null +++ b/powertools-logging-logback/pom.xml @@ -0,0 +1,113 @@ + + + + powertools-parent + software.amazon.lambda + 1.12.3 + + 4.0.0 + + powertools-logging-logback + AWS Lambda Powertools for Java library Logging with LogBack + + A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. + + https://aws.amazon.com/lambda/ + + GitHub Issues + https://github.com/awslabs/aws-lambda-powertools-java/issues + + + https://github.com/awslabs/aws-lambda-powertools-java.git + + + + AWS Lambda Powertools team + Amazon Web Services + https://aws.amazon.com/ + + + + + + ossrh + https://aws.oss.sonatype.org/content/repositories/snapshots + + + + + + + software.amazon.lambda + powertools-logging + ${version} + + + ch.qos.logback + logback-classic + 1.3.4 + provided + + + com.sun.mail + javax.mail + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.apache.commons + commons-lang3 + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-inline + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + com.amazonaws + aws-lambda-java-events + test + + + com.amazonaws + aws-lambda-java-tests + test + + + org.skyscreamer + jsonassert + test + + + + \ No newline at end of file diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaEcsEncoder.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaEcsEncoder.java new file mode 100644 index 000000000..c501c439b --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaEcsEncoder.java @@ -0,0 +1,94 @@ +package software.amazon.lambda.powertools.logging; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.encoder.EncoderBase; +import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.LambdaEcsSerializer; + +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.*; + +/** + * This class will encode the logback event into the format expected by the ECS service (ElasticSearch). + *
+ * Inspired from co.elastic.logging.logback.EcsEncoder, this class doesn't use + * any JSON (de)serialization library (Jackson, Gson, etc.) or Elastic library to avoid the dependency. + *
+ * This encoder also adds cloud information (see doc) + * and Lambda function information (see doc, currently in beta). + */ +public class LambdaEcsEncoder extends EncoderBase { + + protected static final String ECS_VERSION = "1.2.0"; + protected static final String CLOUD_PROVIDER = "aws"; + protected static final String CLOUD_SERVICE = "lambda"; + + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + protected ThrowableHandlingConverter throwableConverter = null; + + @Override + public byte[] headerBytes() { + return null; + } + + @Override + public byte[] encode(ILoggingEvent event) { + Map mdcPropertyMap = event.getMDCPropertyMap(); + + StringBuilder builder = new StringBuilder(256); + LambdaEcsSerializer.serializeObjectStart(builder); + LambdaEcsSerializer.serializeTimestamp(builder, event.getTimeStamp(), "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "UTC"); + LambdaEcsSerializer.serializeEcsVersion(builder, ECS_VERSION); + LambdaEcsSerializer.serializeLogLevel(builder, event.getLevel()); + LambdaEcsSerializer.serializeFormattedMessage(builder, event.getFormattedMessage()); + LambdaEcsSerializer.serializeServiceName(builder, LambdaHandlerProcessor.serviceName()); + LambdaEcsSerializer.serializeServiceVersion(builder, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + // TODO : Environment ? + LambdaEcsSerializer.serializeEventDataset(builder, LambdaHandlerProcessor.serviceName()); + LambdaEcsSerializer.serializeThreadName(builder, event.getThreadName()); + LambdaEcsSerializer.serializeLoggerName(builder, event.getLoggerName()); + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + LambdaEcsSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + LambdaEcsSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); + } else { + LambdaEcsSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert(event)); + } + } + LambdaEcsSerializer.serializeCloudProvider(builder, CLOUD_PROVIDER); + LambdaEcsSerializer.serializeCloudService(builder, CLOUD_SERVICE); + String arn = mdcPropertyMap.get(FUNCTION_ARN.getName()); + if (arn != null) { + String[] arnParts = arn.split(":"); + LambdaEcsSerializer.serializeCloudRegion(builder, arnParts[3]); + LambdaEcsSerializer.serializeCloudAccountId(builder, arnParts[4]); + } + LambdaEcsSerializer.serializeFunctionId(builder, arn); + LambdaEcsSerializer.serializeFunctionName(builder, mdcPropertyMap.get(FUNCTION_NAME.getName())); + LambdaEcsSerializer.serializeFunctionVersion(builder, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + LambdaEcsSerializer.serializeFunctionMemory(builder, mdcPropertyMap.get(FUNCTION_MEMORY_SIZE.getName())); + LambdaEcsSerializer.serializeFunctionExecutionId(builder, mdcPropertyMap.get(FUNCTION_REQUEST_ID.getName())); + LambdaEcsSerializer.serializeColdStart(builder, mdcPropertyMap.get(FUNCTION_COLD_START.getName())); + LambdaEcsSerializer.serializeAdditionalFields(builder, event.getMDCPropertyMap()); + LambdaEcsSerializer.serializeTraceId(builder, mdcPropertyMap.get(FUNCTION_TRACE_ID.getName())); + LambdaEcsSerializer.serializeObjectEnd(builder); + return builder.toString().getBytes(UTF_8); + } + + @Override + public byte[] footerBytes() { + return null; + } + + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } +} diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaJsonEncoder.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaJsonEncoder.java new file mode 100644 index 000000000..7df90a4ad --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/LambdaJsonEncoder.java @@ -0,0 +1,86 @@ +package software.amazon.lambda.powertools.logging; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.encoder.EncoderBase; +import software.amazon.lambda.powertools.logging.internal.LambdaJsonSerializer; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Custom encoder for logback that encodes logs in JSON format. + * It does not use a JSON library but a custom serializer ({@link LambdaJsonSerializer}) to reduce the weight of the library. + */ +public class LambdaJsonEncoder extends EncoderBase { + + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + protected ThrowableHandlingConverter throwableConverter = null; + protected String timestampFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZz"; + protected String timestampFormatTimezoneId = null; + private boolean includeThreadInfo = false; + + @Override + public byte[] headerBytes() { + return null; + } + + @Override + public void start() { + super.start(); + throwableProxyConverter.start(); + if (throwableConverter != null) { + throwableConverter.start(); + } + } + + @Override + public byte[] encode(ILoggingEvent event) { + StringBuilder builder = new StringBuilder(256); + LambdaJsonSerializer.serializeObjectStart(builder); + LambdaJsonSerializer.serializeLogLevel(builder, event.getLevel()); + LambdaJsonSerializer.serializeFormattedMessage(builder, event.getFormattedMessage()); + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + LambdaJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); + } else { + LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert(event)); + } + } + LambdaJsonSerializer.serializePowertools(builder, event.getMDCPropertyMap()); + if (includeThreadInfo) { + LambdaJsonSerializer.serializeThreadName(builder, event.getThreadName()); + LambdaJsonSerializer.serializeThreadId(builder, String.valueOf(Thread.currentThread().getId())); + LambdaJsonSerializer.serializeThreadPriority(builder, String.valueOf(Thread.currentThread().getPriority())); + } + LambdaJsonSerializer.serializeTimestamp(builder, event.getTimeStamp(), timestampFormat, timestampFormatTimezoneId); + LambdaJsonSerializer.serializeObjectEnd(builder); + return builder.toString().getBytes(UTF_8); + } + + @Override + public byte[] footerBytes() { + return null; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + + public void setIncludeThreadInfo(boolean includeThreadInfo) { + this.includeThreadInfo = includeThreadInfo; + } +} diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonUtils.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonUtils.java new file mode 100644 index 000000000..f073050f9 --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonUtils.java @@ -0,0 +1,90 @@ +package software.amazon.lambda.powertools.logging.internal; + +public class JsonUtils { + + protected static void serializeAttribute(StringBuilder builder, String attr, String value, boolean notBegin) { + if (value != null) { + if (notBegin) { + builder.append(", "); + } + builder.append("\"").append(attr).append("\": "); + boolean isString = isString(value); + if (isString) builder.append("\""); + builder.append(value); + if (isString) builder.append("\""); + } + } + + protected static void serializeAttributeAsString(StringBuilder builder, String attr, String value, boolean notBegin) { + if (value != null) { + if (notBegin) { + builder.append(", "); + } + builder.append("\"") + .append(attr) + .append("\": \"") + .append(value) + .append("\""); + } + } + + protected static void serializeAttribute(StringBuilder builder, String attr, String value) { + serializeAttribute(builder, attr, value, true); + } + + protected static void serializeAttributeAsString(StringBuilder builder, String attr, String value) { + serializeAttributeAsString(builder, attr, value, true); + } + + /** + * As MDC is a Map, we need to check the type to output numbers and booleans correctly (without quotes) + */ + private static boolean isString(String str) { + if (str == null) { + return true; + } + if (str.equals("true") || str.equals("false")) { + return false; // boolean + } + return !isNumeric(str); // number + } + + /** + * Taken from commons-lang3 NumberUtils to avoid include the library + */ + private static boolean isNumeric(final String str) { + if (str == null || str.length() == 0) { + return false; + } + if (str.charAt(str.length() - 1) == '.') { + return false; + } + if (str.charAt(0) == '-') { + if (str.length() == 1) { + return false; + } + return withDecimalsParsing(str, 1); + } + return withDecimalsParsing(str, 0); + } + + /** + * Taken from commons-lang3 NumberUtils + */ + private static boolean withDecimalsParsing(final String str, final int beginIdx) { + int decimalPoints = 0; + for (int i = beginIdx; i < str.length(); i++) { + final boolean isDecimalPoint = str.charAt(i) == '.'; + if (isDecimalPoint) { + decimalPoints++; + } + if (decimalPoints > 1) { + return false; + } + if (!isDecimalPoint && !Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsSerializer.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsSerializer.java new file mode 100644 index 000000000..06c2de511 --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsSerializer.java @@ -0,0 +1,171 @@ +package software.amazon.lambda.powertools.logging.internal; + +import ch.qos.logback.classic.Level; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Matcher; + +import static software.amazon.lambda.powertools.logging.internal.JsonUtils.serializeAttributeAsString; + +/** + * This class will serialize the log events in ecs format (ElasticSearch).
+ * + * Inspired from the ElasticSearch Serializer co.elastic.logging.EcsJsonSerializer, this class doesn't use + * any JSON (de)serialization library (Jackson, Gson, etc.) to avoid the dependency + */ +public class LambdaEcsSerializer { + protected static final String TIMESTAMP_ATTR_NAME = "@timestamp"; + protected static final String ECS_VERSION_ATTR_NAME = "ecs.version"; + protected static final String LOGGER_ATTR_NAME = "log.logger"; + protected static final String LEVEL_ATTR_NAME = "log.level"; + protected static final String SERVICE_NAME_ATTR_NAME = "service.name"; + protected static final String SERVICE_VERSION_ATTR_NAME = "service.version"; + protected static final String SERVICE_ENV_ATTR_NAME = "service.environment"; + protected static final String EVENT_DATASET_ATTR_NAME = "event.dataset"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "process.thread.name"; + protected static final String THREAD_ID_ATTR_NAME = "process.thread.id"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "error.message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "error.type"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "error.stack_trace"; + protected static final String CLOUD_PROVIDER_ATTR_NAME = "cloud.provider"; + protected static final String CLOUD_REGION_ATTR_NAME = "cloud.region"; + protected static final String CLOUD_ACCOUNT_ATTR_NAME = "cloud.account.id"; + protected static final String CLOUD_SERVICE_ATTR_NAME = "cloud.service.name"; + protected static final String FUNCTION_COLD_START_ATTR_NAME = "faas.coldstart"; + protected static final String FUNCTION_REQUEST_ID_ATTR_NAME = "faas.execution"; + protected static final String FUNCTION_ARN_ATTR_NAME = "faas.id"; + protected static final String FUNCTION_NAME_ATTR_NAME = "faas.name"; + protected static final String FUNCTION_VERSION_ATTR_NAME = "faas.version"; + protected static final String FUNCTION_MEMORY_ATTR_NAME = "faas.memory"; + protected static final String FUNCTION_TRACE_ID_ATTR_NAME = "trace.id"; + + public static void serializeObjectStart(StringBuilder builder) { + builder.append('{'); + } + + public static void serializeObjectEnd(StringBuilder builder) { + builder.append("}\n"); + } + + public static void serializeTimestamp(StringBuilder builder, long timestamp, String timestampFormat, String timestampFormatTimezoneId) { + String formattedTimestamp; + if (timestampFormat == null || timestamp < 0) { + formattedTimestamp = String.valueOf(timestamp); + } else { + Date date = new Date(timestamp); + DateFormat format = new SimpleDateFormat(timestampFormat); + + if (timestampFormatTimezoneId != null) { + TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); + format.setTimeZone(tz); + } + formattedTimestamp = format.format(date); + } + serializeAttributeAsString(builder, TIMESTAMP_ATTR_NAME, formattedTimestamp, false); + } + + public static void serializeThreadName(StringBuilder builder, String threadName) { + if (threadName != null) { + serializeAttributeAsString(builder, THREAD_ATTR_NAME, threadName); + } + } + + public static void serializeLogLevel(StringBuilder builder, Level level) { + serializeAttributeAsString(builder, LEVEL_ATTR_NAME, level.toString()); + } + + public static void serializeFormattedMessage(StringBuilder builder, String formattedMessage) { + serializeAttributeAsString(builder, FORMATTED_MESSAGE_ATTR_NAME, formattedMessage.replaceAll("\"", Matcher.quoteReplacement("\\\""))); + } + + public static void serializeException(StringBuilder builder, String className, String message, String stackTrace) { + serializeAttributeAsString(builder, EXCEPTION_MSG_ATTR_NAME, message); + serializeAttributeAsString(builder, EXCEPTION_CLASS_ATTR_NAME, className); + serializeAttributeAsString(builder, EXCEPTION_STACK_ATTR_NAME, stackTrace); + } + + public static void serializeException(StringBuilder builder, Throwable throwable) { + serializeException(builder, throwable.getClass().getName(), throwable.getMessage(), Arrays.toString(throwable.getStackTrace())); + } + + public static void serializeThreadId(StringBuilder builder, String threadId) { + serializeAttributeAsString(builder, THREAD_ID_ATTR_NAME, threadId); + } + + public static void serializeAdditionalFields(StringBuilder builder, Map mdc) { + TreeMap sortedMap = new TreeMap<>(mdc); + + sortedMap.forEach((k, v) -> { + if (!PowertoolsLoggedFields.stringValues().contains(k)) { + serializeAttributeAsString(builder, k, v); + } + }); + } + + public static void serializeEcsVersion(StringBuilder builder, String ecsVersion) { + serializeAttributeAsString(builder, ECS_VERSION_ATTR_NAME, ecsVersion); + } + + public static void serializeServiceName(StringBuilder builder, String serviceName) { + serializeAttributeAsString(builder, SERVICE_NAME_ATTR_NAME, serviceName); + } + + public static void serializeServiceVersion(StringBuilder builder, String serviceVersion) { + serializeAttributeAsString(builder, SERVICE_VERSION_ATTR_NAME, serviceVersion); + } + + public static void serializeEventDataset(StringBuilder builder, String serviceName) { + serializeAttributeAsString(builder, EVENT_DATASET_ATTR_NAME, serviceName); + } + + public static void serializeLoggerName(StringBuilder builder, String loggerName) { + serializeAttributeAsString(builder, LOGGER_ATTR_NAME, loggerName); + } + + public static void serializeCloudProvider(StringBuilder builder, String cloudProvider) { + serializeAttributeAsString(builder, CLOUD_PROVIDER_ATTR_NAME, cloudProvider); + } + + public static void serializeCloudService(StringBuilder builder, String cloudService) { + serializeAttributeAsString(builder, CLOUD_SERVICE_ATTR_NAME, cloudService); + } + + public static void serializeCloudRegion(StringBuilder builder, String cloudRegion) { + serializeAttributeAsString(builder, CLOUD_REGION_ATTR_NAME, cloudRegion); + } + + public static void serializeCloudAccountId(StringBuilder builder, String cloudAccountId) { + serializeAttributeAsString(builder, CLOUD_ACCOUNT_ATTR_NAME, cloudAccountId); + } + + public static void serializeColdStart(StringBuilder builder, String coldStart) { + serializeAttributeAsString(builder, FUNCTION_COLD_START_ATTR_NAME, coldStart); + } + + public static void serializeFunctionExecutionId(StringBuilder builder, String requestId) { + serializeAttributeAsString(builder, FUNCTION_REQUEST_ID_ATTR_NAME, requestId); + } + + public static void serializeFunctionId(StringBuilder builder, String functionArn) { + serializeAttributeAsString(builder, FUNCTION_ARN_ATTR_NAME, functionArn); + } + + public static void serializeFunctionName(StringBuilder builder, String functionName) { + serializeAttributeAsString(builder, FUNCTION_NAME_ATTR_NAME, functionName); + } + + public static void serializeFunctionVersion(StringBuilder builder, String functionVersion) { + serializeAttributeAsString(builder, FUNCTION_VERSION_ATTR_NAME, functionVersion); + } + + public static void serializeFunctionMemory(StringBuilder builder, String functionMemory) { + serializeAttributeAsString(builder, FUNCTION_MEMORY_ATTR_NAME, functionMemory); + } + + public static void serializeTraceId(StringBuilder builder, String traceId) { + serializeAttributeAsString(builder, FUNCTION_TRACE_ID_ATTR_NAME, traceId); + } +} diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonSerializer.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonSerializer.java new file mode 100644 index 000000000..1c368679b --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonSerializer.java @@ -0,0 +1,96 @@ +package software.amazon.lambda.powertools.logging.internal; + +import ch.qos.logback.classic.Level; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Matcher; + +import static software.amazon.lambda.powertools.logging.internal.JsonUtils.serializeAttribute; + +/** + * This class will serialize the log events in json.
+ * + * Inspired from the ElasticSearch Serializer {@link co.elastic.logging.EcsJsonSerializer}, this class doesn't use + * any JSON (de)serialization library (Jackson, Gson, etc.) to avoid the dependency + */ +public class LambdaJsonSerializer { + protected static final String TIMESTAMP_ATTR_NAME = "timestamp"; + protected static final String LEVEL_ATTR_NAME = "level"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "thread"; + protected static final String THREAD_ID_ATTR_NAME = "thread_id"; + protected static final String THREAD_PRIORITY_ATTR_NAME = "thread_priority"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "name"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "stack"; + protected static final String EXCEPTION_ATTR_NAME = "error"; + + + public static void serializeObjectStart(StringBuilder builder) { + builder.append('{'); + } + + public static void serializeObjectEnd(StringBuilder builder) { + builder.append("}\n"); + } + + public static void serializeTimestamp(StringBuilder builder, long timestamp, String timestampFormat, String timestampFormatTimezoneId) { + String formattedTimestamp; + if (timestampFormat == null || timestamp < 0) { + formattedTimestamp = String.valueOf(timestamp); + } else { + Date date = new Date(timestamp); + DateFormat format = new SimpleDateFormat(timestampFormat); + + if (timestampFormatTimezoneId != null) { + TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); + format.setTimeZone(tz); + } + formattedTimestamp = format.format(date); + } + serializeAttribute(builder, TIMESTAMP_ATTR_NAME, formattedTimestamp); + } + + public static void serializeThreadName(StringBuilder builder, String threadName) { + if (threadName != null) { + serializeAttribute(builder, THREAD_ATTR_NAME, threadName); + } + } + + public static void serializeLogLevel(StringBuilder builder, Level level) { + serializeAttribute(builder, LEVEL_ATTR_NAME, level.toString(), false); + } + + public static void serializeFormattedMessage(StringBuilder builder, String formattedMessage) { + serializeAttribute(builder, FORMATTED_MESSAGE_ATTR_NAME, formattedMessage.replaceAll("\"", Matcher.quoteReplacement("\\\""))); + } + + public static void serializeException(StringBuilder builder, String className, String message, String stackTrace) { + builder.append("\"").append(EXCEPTION_ATTR_NAME).append("\": {"); + serializeAttribute(builder, EXCEPTION_MSG_ATTR_NAME, message, false); + serializeAttribute(builder, EXCEPTION_CLASS_ATTR_NAME, className); + serializeAttribute(builder, EXCEPTION_STACK_ATTR_NAME, stackTrace); + builder.append("},"); + } + + public static void serializeException(StringBuilder builder, Throwable throwable) { + serializeException(builder, throwable.getClass().getName(), throwable.getMessage(), Arrays.toString(throwable.getStackTrace())); + } + + public static void serializeThreadId(StringBuilder builder, String threadId) { + serializeAttribute(builder, THREAD_ID_ATTR_NAME, threadId); + } + + public static void serializeThreadPriority(StringBuilder builder, String threadPriority) { + serializeAttribute(builder, THREAD_PRIORITY_ATTR_NAME, threadPriority); + } + + public static void serializePowertools(StringBuilder builder, Map mdc) { + TreeMap sortedMap = new TreeMap<>(mdc); + sortedMap.forEach((k, v) -> + serializeAttribute(builder, k, v)); + } + +} diff --git a/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManager.java b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManager.java new file mode 100644 index 000000000..fdf82dd1a --- /dev/null +++ b/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManager.java @@ -0,0 +1,35 @@ +package software.amazon.lambda.powertools.logging.internal; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class LogbackLoggingManager implements LoggingManager { + + private final LoggerContext loggerContext; + + public LogbackLoggingManager() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (!(loggerFactory instanceof LoggerContext)) { + throw new RuntimeException("LoggerFactory does not match required type: " + LoggerContext.class.getName()); + } + loggerContext = (LoggerContext) loggerFactory; + } + + @Override + public void resetLogLevel(org.slf4j.event.Level logLevel) { + List loggers = loggerContext.getLoggerList(); + for (Logger logger : loggers) { + logger.setLevel(Level.convertAnSLF4JLevel(logLevel)); + } + } + + @Override + public org.slf4j.event.Level getLogLevel(org.slf4j.Logger logger) { + return org.slf4j.event.Level.valueOf(loggerContext.getLogger(logger.getName()).getEffectiveLevel().toString()); + } +} diff --git a/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManagerTest.java b/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManagerTest.java new file mode 100644 index 000000000..62a2aa374 --- /dev/null +++ b/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LogbackLoggingManagerTest.java @@ -0,0 +1,37 @@ +package software.amazon.lambda.powertools.logging.internal; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.*; + +public class LogbackLoggingManagerTest { + + private static Logger LOG = LoggerFactory.getLogger(LogbackLoggingManagerTest.class); + private static Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + @Test + @Order(1) + public void getLogLevel_shouldReturnConfiguredLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(INFO); + + logLevel = manager.getLogLevel(ROOT); + assertThat(logLevel).isEqualTo(WARN); + } + + @Test + @Order(2) + public void resetLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + manager.resetLogLevel(ERROR); + + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(ERROR); + } +} diff --git a/powertools-logging-logback/src/test/resources/logback-test.xml b/powertools-logging-logback/src/test/resources/logback-test.xml new file mode 100644 index 000000000..116833c3f --- /dev/null +++ b/powertools-logging-logback/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file