diff --git a/pom.xml b/pom.xml index 1e4c3a9fd..9e38de657 100644 --- a/pom.xml +++ b/pom.xml @@ -501,6 +501,11 @@ org.apache.maven.plugins maven-surefire-plugin + + + on-demand + + org.jacoco @@ -632,6 +637,9 @@ --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED + + on-demand + diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java index 69fc1283a..4c4e8e9db 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java @@ -28,4 +28,6 @@ private LambdaConstants() { public static final String ROOT_EQUALS = "Root="; public static final String POWERTOOLS_SERVICE_NAME = "POWERTOOLS_SERVICE_NAME"; public static final String SERVICE_UNDEFINED = "service_undefined"; + public static final String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; + public static final String ON_DEMAND_INVOCATION_TYPE = "on-demand"; } diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java index 393835d1e..15bff15d6 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java @@ -93,7 +93,14 @@ protected static void resetServiceName() { } public static boolean isColdStart() { - return isColdStart == null; + if (isColdStart != null) { + return isColdStart; + } + + String initType = System.getenv(LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE); + isColdStart = LambdaConstants.ON_DEMAND_INVOCATION_TYPE.equals(initType); + + return isColdStart; } public static void coldStartDone() { diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java index 5c6bdc020..0726a9e77 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java @@ -216,6 +216,7 @@ void extractContext_notKnownHandler() { } @Test + @SetEnvironmentVariable(key = LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE, value = LambdaConstants.ON_DEMAND_INVOCATION_TYPE) void isColdStart() { boolean isColdStart = LambdaHandlerProcessor.isColdStart(); diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml index a9cf5739b..33298731b 100644 --- a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml +++ b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml @@ -104,7 +104,6 @@ test - generate-graalvm-files diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java index b5c816286..26e5ec5d9 100644 --- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java @@ -47,342 +47,358 @@ import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; /** - * These test are using DynamoDBLocal and sqlite, see https://nickolasfisher.com/blog/Configuring-an-In-Memory-DynamoDB-instance-with-Java-for-Integration-Testing + * These test are using DynamoDBLocal and sqlite, see + * https://www.nickolasfisher.com/blog/configuring-an-in-memory-dynamodb-instance-with-java-for-integration-testing/ * NOTE: on a Mac with Apple Chipset, you need to use the Oracle JDK x86 64-bit */ class DynamoDBPersistenceStoreTest extends DynamoDBConfig { - protected static final String TABLE_NAME_CUSTOM = "idempotency_table_custom"; - private Map key; - private DynamoDBPersistenceStore dynamoDBPersistenceStore; - - @Test - void putRecord_shouldCreateRecordInDynamoDB() throws IdempotencyItemAlreadyExistsException { - Instant now = Instant.now(); - long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - dynamoDBPersistenceStore.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); - - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - Map item = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(item).isNotNull(); - assertThat(item.get("status").s()).isEqualTo("COMPLETED"); - assertThat(item.get("expiration").n()).isEqualTo(String.valueOf(expiry)); - } - - @Test - void putRecord_shouldCreateRecordInDynamoDB_IfPreviousExpired() { - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - - // GIVEN: Insert a fake item with same id and expired - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.minus(30, ChronoUnit.SECONDS).getEpochSecond(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); - item.put("data", AttributeValue.builder().s("Fake Data").build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - - // WHEN: call putRecord - long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - dynamoDBPersistenceStore.putRecord( - new DataRecord("key", - DataRecord.Status.INPROGRESS, - expiry2, - null, - null), - now); - - // THEN: an item is inserted - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(itemInDb).isNotNull(); - assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); - assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2)); - } - - @Test - void putRecord_shouldCreateRecordInDynamoDB_IfLambdaWasInProgressAndTimedOut() { - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - - // GIVEN: Insert a fake item with same id and progress expired (Lambda timed out before and we allow a new - // execution) - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); - long progressExpiry = now.minus(30, ChronoUnit.SECONDS).toEpochMilli(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); - item.put("data", AttributeValue.builder().s("Fake Data").build()); - item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - - // WHEN: call putRecord - long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - dynamoDBPersistenceStore.putRecord( - new DataRecord("key", - DataRecord.Status.INPROGRESS, - expiry2, - null, - null), - now); - - // THEN: an item is inserted - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(itemInDb).isNotNull(); - assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); - assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2)); - } - - @Test - void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() { - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - - // GIVEN: Insert a fake item with same id - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); // not expired - item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); - item.put("data", AttributeValue.builder().s("Fake Data").build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - - // WHEN: call putRecord - long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - DataRecord recordToInsert = new DataRecord("key", - DataRecord.Status.INPROGRESS, - expiry2, - null, - null); - assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now)) - .isInstanceOf(IdempotencyItemAlreadyExistsException.class) - // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD") - .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); - - // THEN: item was not updated, retrieve the initial one - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(itemInDb).isNotNull(); - assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED"); - assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); - assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data"); - } - - @Test - void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() { - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - - // GIVEN: Insert a fake item with same id - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired - long progressExpiry = now.plus(30, ChronoUnit.SECONDS).toEpochMilli(); // not expired - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); - item.put("data", AttributeValue.builder().s("Fake Data").build()); - item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - - // WHEN: call putRecord - long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - DataRecord recordToInsert = new DataRecord("key", - DataRecord.Status.INPROGRESS, - expiry2, - "Fake Data 2", - null); - assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now)) - .isInstanceOf(IdempotencyItemAlreadyExistsException.class) - // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD") - .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); - - // THEN: item was not updated, retrieve the initial one - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(itemInDb).isNotNull(); - assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); - assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); - assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data"); - } - - @Test - void getRecord_shouldReturnExistingRecord() throws IdempotencyItemNotFoundException { - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - - // GIVEN: Insert a fake item with same id - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); - item.put("data", AttributeValue.builder().s("Fake Data").build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - - // WHEN - DataRecord dr = dynamoDBPersistenceStore.getRecord("key"); - - // THEN - assertThat(dr.getIdempotencyKey()).isEqualTo("key"); - assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); - assertThat(dr.getResponseData()).isEqualTo("Fake Data"); - assertThat(dr.getExpiryTimestamp()).isEqualTo(expiry); - } - - @Test - void getRecord_shouldThrowException_whenRecordIsAbsent() { - assertThatThrownBy(() -> dynamoDBPersistenceStore.getRecord("key")) - .isInstanceOf(IdempotencyItemNotFoundException.class); - } - - @Test - void updateRecord_shouldUpdateRecord() { - // GIVEN: Insert a fake item with same id - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - // enable payload validation - dynamoDBPersistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(), - null); - - // WHEN - expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); - DataRecord dr = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Fake result", "hash"); - dynamoDBPersistenceStore.updateRecord(dr); - - // THEN - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); - assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED"); - assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); - assertThat(itemInDb.get("data").s()).isEqualTo("Fake result"); - assertThat(itemInDb.get("validation").s()).isEqualTo("hash"); - } - - @Test - void deleteRecord_shouldDeleteRecord() { - // GIVEN: Insert a fake item with same id - key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); - Map item = new HashMap<>(key); - Instant now = Instant.now(); - long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); - item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); - item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); - client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); - assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isEqualTo(1); - - // WHEN - dynamoDBPersistenceStore.deleteRecord("key"); - - // THEN - assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isZero(); - } - - @Test - void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFoundException { - try { - client.createTable(CreateTableRequest.builder() - .tableName(TABLE_NAME_CUSTOM) - .keySchema( - KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("key").build(), - KeySchemaElement.builder().keyType(KeyType.RANGE).attributeName("sortkey").build()) - .attributeDefinitions( - AttributeDefinition.builder().attributeName("key").attributeType(ScalarAttributeType.S) - .build(), - AttributeDefinition.builder().attributeName("sortkey").attributeType(ScalarAttributeType.S) - .build()) - .billingMode(BillingMode.PAY_PER_REQUEST) - .build()); - - DynamoDBPersistenceStore persistenceStore = DynamoDBPersistenceStore.builder() - .withTableName(TABLE_NAME_CUSTOM) - .withDynamoDbClient(client) - .withDataAttr("result") - .withExpiryAttr("expiry") - .withKeyAttr("key") - .withSortKeyAttr("sortkey") - .withStaticPkValue("pk") - .withStatusAttr("state") - .withValidationAttr("valid") - .build(); - - Instant now = Instant.now(); - DataRecord dr = new DataRecord( - "mykey", - DataRecord.Status.INPROGRESS, - now.plus(400, ChronoUnit.SECONDS).getEpochSecond(), - null, - null); - // PUT - persistenceStore.putRecord(dr, now); - - Map customKey = new HashMap<>(); - customKey.put("key", AttributeValue.builder().s("pk").build()); - customKey.put("sortkey", AttributeValue.builder().s("mykey").build()); - - Map itemInDb = client - .getItem(GetItemRequest.builder().tableName(TABLE_NAME_CUSTOM).key(customKey).build()).item(); - - // GET - DataRecord recordInDb = persistenceStore.getRecord("mykey"); - - assertThat(itemInDb).isNotNull(); - assertThat(itemInDb.get("key").s()).isEqualTo("pk"); - assertThat(itemInDb.get("sortkey").s()).isEqualTo(recordInDb.getIdempotencyKey()); - assertThat(itemInDb.get("state").s()).isEqualTo(recordInDb.getStatus().toString()); - assertThat(itemInDb.get("expiry").n()).isEqualTo(String.valueOf(recordInDb.getExpiryTimestamp())); - - // UPDATE - DataRecord updatedRecord = new DataRecord( - "mykey", - DataRecord.Status.COMPLETED, - now.plus(500, ChronoUnit.SECONDS).getEpochSecond(), - "response", - null); - persistenceStore.updateRecord(updatedRecord); - recordInDb = persistenceStore.getRecord("mykey"); - assertThat(recordInDb).isEqualTo(updatedRecord); - - // DELETE - persistenceStore.deleteRecord("mykey"); - assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME_CUSTOM).build()).count()).isEqualTo(0); - - } finally { - try { - client.deleteTable(DeleteTableRequest.builder().tableName(TABLE_NAME_CUSTOM).build()); - } catch (Exception e) { - // OK - } + protected static final String TABLE_NAME_CUSTOM = "idempotency_table_custom"; + private Map key; + private DynamoDBPersistenceStore dynamoDBPersistenceStore; + + @Test + void putRecord_shouldCreateRecordInDynamoDB() throws IdempotencyItemAlreadyExistsException { + Instant now = Instant.now(); + long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + dynamoDBPersistenceStore + .putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); + + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + Map item = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(item).isNotNull(); + assertThat(item.get("status").s()).isEqualTo("COMPLETED"); + assertThat(item.get("expiration").n()).isEqualTo(String.valueOf(expiry)); + } + + @Test + void putRecord_shouldCreateRecordInDynamoDB_IfPreviousExpired() { + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + + // GIVEN: Insert a fake item with same id and expired + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.minus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); + item.put("data", AttributeValue.builder().s("Fake Data").build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + + // WHEN: call putRecord + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + dynamoDBPersistenceStore.putRecord( + new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null), + now); + + // THEN: an item is inserted + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(itemInDb).isNotNull(); + assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); + assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2)); + } + + @Test + void putRecord_shouldCreateRecordInDynamoDB_IfLambdaWasInProgressAndTimedOut() { + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + + // GIVEN: Insert a fake item with same id and progress expired (Lambda timed out + // before and we allow a new + // execution) + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + long progressExpiry = now.minus(30, ChronoUnit.SECONDS).toEpochMilli(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); + item.put("data", AttributeValue.builder().s("Fake Data").build()); + item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + + // WHEN: call putRecord + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + dynamoDBPersistenceStore.putRecord( + new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null), + now); + + // THEN: an item is inserted + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(itemInDb).isNotNull(); + assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); + assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2)); + } + + @Test + void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() { + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + + // GIVEN: Insert a fake item with same id + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); // not expired + item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); + item.put("data", AttributeValue.builder().s("Fake Data").build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + + // WHEN: call putRecord + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord recordToInsert = new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null); + assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now)) + .isInstanceOf(IdempotencyItemAlreadyExistsException.class) + // DataRecord should be present due to + // returnValuesOnConditionCheckFailure("ALL_OLD") + .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); + + // THEN: item was not updated, retrieve the initial one + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(itemInDb).isNotNull(); + assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED"); + assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); + assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data"); + } + + @Test + void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() { + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + + // GIVEN: Insert a fake item with same id + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired + long progressExpiry = now.plus(30, ChronoUnit.SECONDS).toEpochMilli(); // not expired + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); + item.put("data", AttributeValue.builder().s("Fake Data").build()); + item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + + // WHEN: call putRecord + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord recordToInsert = new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + "Fake Data 2", + null); + assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now)) + .isInstanceOf(IdempotencyItemAlreadyExistsException.class) + // DataRecord should be present due to + // returnValuesOnConditionCheckFailure("ALL_OLD") + .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); + + // THEN: item was not updated, retrieve the initial one + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(itemInDb).isNotNull(); + assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); + assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); + assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data"); + } + + @Test + void getRecord_shouldReturnExistingRecord() throws IdempotencyItemNotFoundException { + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + + // GIVEN: Insert a fake item with same id + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build()); + item.put("data", AttributeValue.builder().s("Fake Data").build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + + // WHEN + DataRecord dr = dynamoDBPersistenceStore.getRecord("key"); + + // THEN + assertThat(dr.getIdempotencyKey()).isEqualTo("key"); + assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); + assertThat(dr.getResponseData()).isEqualTo("Fake Data"); + assertThat(dr.getExpiryTimestamp()).isEqualTo(expiry); + } + + @Test + void getRecord_shouldThrowException_whenRecordIsAbsent() { + assertThatThrownBy(() -> dynamoDBPersistenceStore.getRecord("key")) + .isInstanceOf(IdempotencyItemNotFoundException.class); + } + + @Test + void updateRecord_shouldUpdateRecord() { + // GIVEN: Insert a fake item with same id + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + // enable payload validation + dynamoDBPersistenceStore.configure( + IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(), + null); + + // WHEN + expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord dr = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Fake result", "hash"); + dynamoDBPersistenceStore.updateRecord(dr); + + // THEN + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED"); + assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); + assertThat(itemInDb.get("data").s()).isEqualTo("Fake result"); + assertThat(itemInDb.get("validation").s()).isEqualTo("hash"); + } + + @Test + void deleteRecord_shouldDeleteRecord() { + // GIVEN: Insert a fake item with same id + key = Collections.singletonMap("id", AttributeValue.builder().s("key").build()); + Map item = new HashMap<>(key); + Instant now = Instant.now(); + long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); + item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); + item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build()); + client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build()); + assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isEqualTo(1); + + // WHEN + dynamoDBPersistenceStore.deleteRecord("key"); + + // THEN + assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isZero(); + } + + @Test + void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFoundException { + try { + client.createTable(CreateTableRequest.builder() + .tableName(TABLE_NAME_CUSTOM) + .keySchema( + KeySchemaElement.builder().keyType(KeyType.HASH) + .attributeName("key").build(), + KeySchemaElement.builder().keyType(KeyType.RANGE) + .attributeName("sortkey").build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("key") + .attributeType(ScalarAttributeType.S) + .build(), + AttributeDefinition.builder().attributeName("sortkey") + .attributeType(ScalarAttributeType.S) + .build()) + .billingMode(BillingMode.PAY_PER_REQUEST) + .build()); + + DynamoDBPersistenceStore persistenceStore = DynamoDBPersistenceStore.builder() + .withTableName(TABLE_NAME_CUSTOM) + .withDynamoDbClient(client) + .withDataAttr("result") + .withExpiryAttr("expiry") + .withKeyAttr("key") + .withSortKeyAttr("sortkey") + .withStaticPkValue("pk") + .withStatusAttr("state") + .withValidationAttr("valid") + .build(); + + Instant now = Instant.now(); + DataRecord dr = new DataRecord( + "mykey", + DataRecord.Status.INPROGRESS, + now.plus(400, ChronoUnit.SECONDS).getEpochSecond(), + null, + null); + // PUT + persistenceStore.putRecord(dr, now); + + Map customKey = new HashMap<>(); + customKey.put("key", AttributeValue.builder().s("pk").build()); + customKey.put("sortkey", AttributeValue.builder().s("mykey").build()); + + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME_CUSTOM).key(customKey) + .build()) + .item(); + + // GET + DataRecord recordInDb = persistenceStore.getRecord("mykey"); + + assertThat(itemInDb).isNotNull(); + assertThat(itemInDb.get("key").s()).isEqualTo("pk"); + assertThat(itemInDb.get("sortkey").s()).isEqualTo(recordInDb.getIdempotencyKey()); + assertThat(itemInDb.get("state").s()).isEqualTo(recordInDb.getStatus().toString()); + assertThat(itemInDb.get("expiry").n()) + .isEqualTo(String.valueOf(recordInDb.getExpiryTimestamp())); + + // UPDATE + DataRecord updatedRecord = new DataRecord( + "mykey", + DataRecord.Status.COMPLETED, + now.plus(500, ChronoUnit.SECONDS).getEpochSecond(), + "response", + null); + persistenceStore.updateRecord(updatedRecord); + recordInDb = persistenceStore.getRecord("mykey"); + assertThat(recordInDb).isEqualTo(updatedRecord); + + // DELETE + persistenceStore.deleteRecord("mykey"); + assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME_CUSTOM).build()).count()) + .isEqualTo(0); + + } finally { + try { + client.deleteTable(DeleteTableRequest.builder().tableName(TABLE_NAME_CUSTOM).build()); + } catch (Exception e) { + // OK + } + } + } + + @Test + @SetEnvironmentVariable(key = Constants.IDEMPOTENCY_DISABLED_ENV, value = "true") + void idempotencyDisabled_noClientShouldBeCreated() { + DynamoDBPersistenceStore store = DynamoDBPersistenceStore.builder().withTableName(TABLE_NAME).build(); + assertThatThrownBy(() -> store.getRecord("fake")) + .isInstanceOf(NullPointerException.class); + } + + @BeforeEach + void setup() { + dynamoDBPersistenceStore = DynamoDBPersistenceStore.builder() + .withTableName(TABLE_NAME) + .withDynamoDbClient(client) + .build(); + } + + @AfterEach + void emptyDB() { + // Clear all items from the table + client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()) + .items() + .forEach(item -> { + Map itemKey = Collections.singletonMap("id", + item.get("id")); + client.deleteItem(DeleteItemRequest.builder().tableName(TABLE_NAME).key(itemKey) + .build()); + }); + key = null; } - } - - @Test - @SetEnvironmentVariable(key = Constants.IDEMPOTENCY_DISABLED_ENV, value = "true") - void idempotencyDisabled_noClientShouldBeCreated() { - DynamoDBPersistenceStore store = DynamoDBPersistenceStore.builder().withTableName(TABLE_NAME).build(); - assertThatThrownBy(() -> store.getRecord("fake")) - .isInstanceOf(NullPointerException.class); - } - - @BeforeEach - void setup() { - dynamoDBPersistenceStore = DynamoDBPersistenceStore.builder() - .withTableName(TABLE_NAME) - .withDynamoDbClient(client) - .build(); - } - - @AfterEach - void emptyDB() { - // Clear all items from the table - client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()) - .items() - .forEach(item -> { - Map itemKey = Collections.singletonMap("id", item.get("id")); - client.deleteItem(DeleteItemRequest.builder().tableName(TABLE_NAME).key(itemKey).build()); - }); - key = null; - } } diff --git a/powertools-logging/powertools-logging-log4j/pom.xml b/powertools-logging/powertools-logging-log4j/pom.xml index 1cf3bf265..6e0113ea6 100644 --- a/powertools-logging/powertools-logging-log4j/pom.xml +++ b/powertools-logging/powertools-logging-log4j/pom.xml @@ -120,6 +120,9 @@ --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED + + on-demand + diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java index 46b5b65d4..4716f666a 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java @@ -79,7 +79,7 @@ void shouldLogInJsonFormat() { File logFile = new File("target/logfile.json"); assertThat(contentOf(logFile)).contains( "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":\"1\",\"service\":\"testLog4j\",\"timestamp\":") - .contains("\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + .contains("\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\r\n"); } @Test @@ -89,7 +89,7 @@ void shouldLogInEcsFormat() { File logFile = new File("target/ecslogfile.json"); assertThat(contentOf(logFile)).contains( - "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLog4j\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-central-1\",\"cloud.account.id\":\"123456789012\",\"faas.id\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"faas.name\":\"test-function\",\"faas.version\":\"1\",\"faas.memory\":128,\"faas.execution\":\"test-request-id\",\"faas.coldstart\":true,\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLog4j\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-central-1\",\"cloud.account.id\":\"123456789012\",\"faas.id\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"faas.name\":\"test-function\",\"faas.version\":\"1\",\"faas.memory\":128,\"faas.execution\":\"test-request-id\",\"faas.coldstart\":true,\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\r\n"); } } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java index 30ede8ba8..b489dcb5a 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java @@ -163,7 +163,7 @@ void shouldLogException() { // THEN (stack is logged with root cause first) assertThat(result).contains( - "\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"java.lang.IllegalStateException: Unexpected value\\n"); + "\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"java.lang.IllegalStateException: Unexpected value\\r\\n"); } private void setMDC() { diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java index 16bd9e92a..0008c98ee 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java @@ -68,378 +68,384 @@ @Order(2) class LambdaJsonEncoderTest { - private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaJsonEncoderTest.class.getName()); - private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); - - private Context context; - - @BeforeEach - void setUp() throws IllegalAccessException, IOException { - MDC.clear(); - // Reset cold start state - writeStaticField(LambdaHandlerProcessor.class, "isColdStart", null, true); - writeStaticField(PowertoolsLogging.class, "hasBeenInitialized", new AtomicBoolean(false), true); - - context = new TestLambdaContext(); - // Make sure file is cleaned up before running tests - try { - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - } catch (NoSuchFileException e) { - // file may not exist on the first launch + private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaJsonEncoderTest.class.getName()); + private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); + + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + MDC.clear(); + // Reset cold start state + writeStaticField(LambdaHandlerProcessor.class, "isColdStart", null, true); + writeStaticField(PowertoolsLogging.class, "hasBeenInitialized", new AtomicBoolean(false), true); + + context = new TestLambdaContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0) + .close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } } - } - - @AfterEach - void cleanUp() throws IOException { - try { - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - } catch (NoSuchFileException e) { - // file may not exist on the first launch + + @AfterEach + void cleanUp() throws IOException { + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0) + .close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @Test + void shouldLogInJsonFormat() { + // GIVEN + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + + // WHEN + handler.handleRequest("Input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains( + "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":"); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingRawJson() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-central-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains( + "\"input\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + // Should auto-escape double quotes around id + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") + .contains("\"correlation_id\":\"1212abcd\""); + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingKeyValue() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-central-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains( + "\"input\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + // Should auto-escape double quotes around id + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") + .contains("\"correlation_id\":\"1212abcd\""); + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + @Test + void shouldNotLogPowertoolsInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + + MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); + MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); + MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); + MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), + String.valueOf(context.getMemoryLimitInMB())); + MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); + MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); + MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains( + "{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"timestamp\":"); + + // WHEN (powertoolsInfo = false) + encoder.setIncludePowertoolsInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no powertools info in logs) + assertThat(result).doesNotContain("cold_start", "function_arn", "function_memory_size", "function_name", + "function_request_id", "function_version", "sampling_rate", "service"); + } + + @Test + void shouldLogStructuredArgumentsAsNewEntries() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-central-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + StructuredArgument argument = StructuredArguments.entry("msg", msg); + + // WHEN + LoggingEvent structuredLoggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "A message", null, + new Object[] { argument }); + byte[] encoded = encoder.encode(structuredLoggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (logged as JSON) + assertThat(result) + .contains( + "\"message\":\"A message\",\"msg\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}"); + } + + @Test + void shouldLogEventForHandlerWithLogEventAnnotation() { + // GIVEN + PowertoolsLogEvent requestHandler = new PowertoolsLogEvent(); + + // WHEN + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("\"event\":[\"ListOfOneElement\"]"); } - } - - @Test - void shouldLogInJsonFormat() { - // GIVEN - PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); - - // WHEN - handler.handleRequest("Input", context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains( - "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":"); - } - - @Test - void shouldLogArgumentsAsJsonWhenUsingRawJson() { - // GIVEN - PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); - SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); - msg.setMessageId("1212abcd"); - msg.setBody("plop"); - msg.setEventSource("eb"); - msg.setAwsRegion("eu-central-1"); - SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); - attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); - msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); - - // WHEN - requestHandler.handleRequest(msg, context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains( - "\"input\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") - .contains("\"message\":\"1212abcd\"") - // Should auto-escape double quotes around id - .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") - .contains("\"correlation_id\":\"1212abcd\""); - // Reserved keys should be ignored - PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { - assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); - assertThat(contentOf(logFile)).contains( - "\"message\":\"Attempted to use reserved key '" + reservedKey - + "' in structured argument. This key will be ignored.\""); - }); - } - - @Test - void shouldLogArgumentsAsJsonWhenUsingKeyValue() { - // GIVEN - PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); - SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); - msg.setMessageId("1212abcd"); - msg.setBody("plop"); - msg.setEventSource("eb"); - msg.setAwsRegion("eu-central-1"); - SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); - attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); - msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); - - // WHEN - requestHandler.handleRequest(msg, context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains( - "\"input\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") - .contains("\"message\":\"1212abcd\"") - // Should auto-escape double quotes around id - .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") - .contains("\"correlation_id\":\"1212abcd\""); - // Reserved keys should be ignored - PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { - assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); - assertThat(contentOf(logFile)).contains( - "\"message\":\"Attempted to use reserved key '" + reservedKey - + "' in structured argument. This key will be ignored.\""); - }); - } - - @Test - void shouldNotLogPowertoolsInfo() { - // GIVEN - LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - - MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); - MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); - MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); - MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), - String.valueOf(context.getMemoryLimitInMB())); - MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); - MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); - MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); - MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); - - // WHEN - byte[] encoded = encoder.encode(loggingEvent); - String result = new String(encoded, StandardCharsets.UTF_8); - - // THEN - assertThat(result).contains( - "{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"timestamp\":"); - - // WHEN (powertoolsInfo = false) - encoder.setIncludePowertoolsInfo(false); - encoded = encoder.encode(loggingEvent); - result = new String(encoded, StandardCharsets.UTF_8); - - // THEN (no powertools info in logs) - assertThat(result).doesNotContain("cold_start", "function_arn", "function_memory_size", "function_name", - "function_request_id", "function_version", "sampling_rate", "service"); - } - - @Test - void shouldLogStructuredArgumentsAsNewEntries() { - // GIVEN - LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - - SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); - msg.setMessageId("1212abcd"); - msg.setBody("plop"); - msg.setEventSource("eb"); - msg.setAwsRegion("eu-central-1"); - SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); - attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); - msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); - StructuredArgument argument = StructuredArguments.entry("msg", msg); - - // WHEN - LoggingEvent structuredLoggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "A message", null, - new Object[] { argument }); - byte[] encoded = encoder.encode(structuredLoggingEvent); - String result = new String(encoded, StandardCharsets.UTF_8); - - // THEN (logged as JSON) - assertThat(result) - .contains( - "\"message\":\"A message\",\"msg\":{\"awsRegion\":\"eu-central-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}"); - } - - @Test - void shouldLogEventForHandlerWithLogEventAnnotation() { - // GIVEN - PowertoolsLogEvent requestHandler = new PowertoolsLogEvent(); - - // WHEN - requestHandler.handleRequest(singletonList("ListOfOneElement"), context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("\"event\":[\"ListOfOneElement\"]"); - } - - @Test - void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { - try { - // GIVEN - LoggingConstants.POWERTOOLS_LOG_EVENT = true; - - PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); - - SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); - message.setBody("body"); - message.setMessageId("1234abcd"); - message.setAwsRegion("eu-central-1"); - - // WHEN - requestHandler.handleRequest(message, context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains("\"message\":\"Handler Event\"") - .contains( - "\"event\":{\"awsRegion\":\"eu-central-1\",\"body\":\"body\",\"messageId\":\"1234abcd\"}"); - } finally { - LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + @Test + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); + message.setBody("body"); + message.setMessageId("1234abcd"); + message.setAwsRegion("eu-central-1"); + + // WHEN + requestHandler.handleRequest(message, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + .contains( + "\"event\":{\"awsRegion\":\"eu-central-1\",\"body\":\"body\",\"messageId\":\"1234abcd\"}"); + } finally { + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + } } - } - - @Test - void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { - // GIVEN - LoggingConstants.POWERTOOLS_LOG_EVENT = false; - - // WHEN - PowertoolsLogEventDisabled requestHandler = new PowertoolsLogEventDisabled(); - requestHandler.handleRequest(singletonList("ListOfOneElement"), context); - - // THEN - Assertions.assertEquals(0, - Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); - } - - @Test - void shouldLogEventAsStringForStreamHandler() throws IOException { - // GIVEN - PowertoolsLogEventForStream requestStreamHandler = new PowertoolsLogEventForStream(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - - // WHEN - requestStreamHandler.handleRequest( - new ByteArrayInputStream( - new ObjectMapper().writeValueAsBytes(Collections.singletonMap("key", "value"))), - output, context); - - // THEN - assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) - .isNotEmpty(); - - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains("\"message\":\"Handler Event\"") - // logged as String for StreamHandler (should auto-escape double-quotes to avoid breaking JSON format) - .contains("\"event\":\"{\\\"key\\\":\\\"value\\\"}\""); - } - - @Test - void shouldLogResponseForHandlerWithLogResponseAnnotation() { - // GIVEN - PowertoolsLogResponse requestHandler = new PowertoolsLogResponse(); - - // WHEN - requestHandler.handleRequest("input", context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains("\"message\":\"Handler Response\"") - .contains("\"response\":\"Hola mundo\""); - } - - @Test - void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { - try { - // GIVEN - LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; - - PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); - - // WHEN - requestHandler.handleRequest("input", context); - - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains("\"message\":\"Handler Response\"") - .contains("\"response\":\"Bonjour le monde\""); - } finally { - LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + + @Test + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + // WHEN + PowertoolsLogEventDisabled requestHandler = new PowertoolsLogEventDisabled(); + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + Assertions.assertEquals(0, + Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + } + + @Test + void shouldLogEventAsStringForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogEventForStream requestStreamHandler = new PowertoolsLogEventForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + // WHEN + requestStreamHandler.handleRequest( + new ByteArrayInputStream( + new ObjectMapper().writeValueAsBytes( + Collections.singletonMap("key", "value"))), + output, context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isNotEmpty(); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + // logged as String for StreamHandler (should auto-escape double-quotes to avoid + // breaking JSON format) + .contains("\"event\":\"{\\\"key\\\":\\\"value\\\"}\""); + } + + @Test + void shouldLogResponseForHandlerWithLogResponseAnnotation() { + // GIVEN + PowertoolsLogResponse requestHandler = new PowertoolsLogResponse(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Hola mundo\""); + } + + @Test + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Bonjour le monde\""); + } finally { + LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + } + } + + @Test + void shouldLogResponseForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogResponseForStream requestStreamHandler = new PowertoolsLogResponseForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + String input = "BobThe Sponge"; + + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), + output, + context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo(input); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"" + input + "\""); + } + + @Test + void shouldLogThreadInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.setIncludeThreadInfo(true); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains( + "\"thread\":\"main\",\"thread_id\":" + Thread.currentThread().getId() + + ",\"thread_priority\":5"); + } + + @Test + void shouldLogTimestampDifferently() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + String pattern = "yyyy-MM-dd_HH"; + String timeZone = "Europe/Paris"; + encoder.setTimestampFormat(pattern); + encoder.setTimestampFormatTimezoneId(timeZone); + + // WHEN + Date date = new Date(); + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); + assertThat(result).contains("\"timestamp\":\"" + simpleDateFormat.format(date) + "\""); + } + + @Test + void shouldLogException() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.start(); + LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", + new IllegalStateException("Unexpected value"), null); + + // WHEN + byte[] encoded = encoder.encode(errorloggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"message\":\"Error\",\"error\":{") + .contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains( + "\"stack\":\"[software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest.shouldLogException"); + + // WHEN (configure a custom throwableConverter) + encoder = new LambdaJsonEncoder(); + RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); + encoder.setThrowableConverter(throwableConverter); + encoder.start(); + encoded = encoder.encode(errorloggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (stack is logged with root cause first) + assertThat(result).contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\\r\\n"); } - } - - @Test - void shouldLogResponseForStreamHandler() throws IOException { - // GIVEN - PowertoolsLogResponseForStream requestStreamHandler = new PowertoolsLogResponseForStream(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - String input = "BobThe Sponge"; - - // WHEN - requestStreamHandler.handleRequest(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), output, - context); - - // THEN - assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) - .isEqualTo(input); - - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) - .contains("\"message\":\"Handler Response\"") - .contains("\"response\":\"" + input + "\""); - } - - @Test - void shouldLogThreadInfo() { - // GIVEN - LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - encoder.setIncludeThreadInfo(true); - - // WHEN - byte[] encoded = encoder.encode(loggingEvent); - String result = new String(encoded, StandardCharsets.UTF_8); - - // THEN - assertThat(result).contains( - "\"thread\":\"main\",\"thread_id\":" + Thread.currentThread().getId() + ",\"thread_priority\":5"); - } - - @Test - void shouldLogTimestampDifferently() { - // GIVEN - LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - String pattern = "yyyy-MM-dd_HH"; - String timeZone = "Europe/Paris"; - encoder.setTimestampFormat(pattern); - encoder.setTimestampFormatTimezoneId(timeZone); - - // WHEN - Date date = new Date(); - byte[] encoded = encoder.encode(loggingEvent); - String result = new String(encoded, StandardCharsets.UTF_8); - - // THEN - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); - assertThat(result).contains("\"timestamp\":\"" + simpleDateFormat.format(date) + "\""); - } - - @Test - void shouldLogException() { - // GIVEN - LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - encoder.start(); - LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", - new IllegalStateException("Unexpected value"), null); - - // WHEN - byte[] encoded = encoder.encode(errorloggingEvent); - String result = new String(encoded, StandardCharsets.UTF_8); - - // THEN - assertThat(result).contains("\"message\":\"Error\",\"error\":{") - .contains("\"message\":\"Unexpected value\"") - .contains("\"name\":\"java.lang.IllegalStateException\"") - .contains( - "\"stack\":\"[software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest.shouldLogException"); - - // WHEN (configure a custom throwableConverter) - encoder = new LambdaJsonEncoder(); - RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); - encoder.setThrowableConverter(throwableConverter); - encoder.start(); - encoded = encoder.encode(errorloggingEvent); - result = new String(encoded, StandardCharsets.UTF_8); - - // THEN (stack is logged with root cause first) - assertThat(result).contains("\"message\":\"Unexpected value\"") - .contains("\"name\":\"java.lang.IllegalStateException\"") - .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\\n"); - } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index fac85e230..e09a953bc 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -131,7 +131,8 @@ void shouldEvictMultipleSmallEventsForLargeValidEvent() { Deque events = buffer.removeAll("key1"); // Should only contain the last few small events plus the large event - // 18 bytes for large event leaves 2 bytes, so only "ij" should remain with the large event + // 18 bytes for large event leaves 2 bytes, so only "ij" should remain with the + // large event assertThat(events).containsExactly("ij", "123456789012345678"); } @@ -353,7 +354,8 @@ void shouldUseDefaultWarningLoggerWhenNotProvided() { // Assert System.err received the warning assertThat(errCapture) .hasToString( - "WARN [KeyBuffer] - Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.\n"); + "WARN [KeyBuffer] - Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer." + + System.lineSeparator()); } finally { System.setErr(originalErr); }