From 0fc57a1f1eacb6e70fd9e82db72c61f75128934a Mon Sep 17 00:00:00 2001 From: Moiz Sharaf Date: Wed, 8 Jan 2025 14:35:07 +0100 Subject: [PATCH 1/8] feat(cfn-custom-resource): add optional 'reason' field for detailed failure reporting in CloudFormation custom resource responses --- .../CloudFormationResponse.java | 5 ++- .../powertools/cloudformation/Response.java | 39 +++++++++++++++++++ .../CloudFormationResponseTest.java | 23 +++++++++++ .../cloudformation/ResponseTest.java | 23 +++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 2f020aa25..404137802 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.utils.StringInputStream; +import software.amazon.awssdk.utils.StringUtils; /** * Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3 @@ -148,7 +149,9 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { - + if (!StringUtils.isBlank(resp.getReason())) { + reason = resp.getReason(); + } String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : context.getLogStreamName(); diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index fe18000d4..94372ac97 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import software.amazon.awssdk.utils.StringUtils; /** * Models the arbitrary data to be sent to the custom resource in response to a CloudFormation event. This object @@ -30,12 +31,22 @@ public class Response { private final Status status; private final String physicalResourceId; private final boolean noEcho; + private final String reason; private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) { this.jsonNode = jsonNode; this.status = status; this.physicalResourceId = physicalResourceId; this.noEcho = noEcho; + this.reason = null; + } + + private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho, String reason) { + this.jsonNode = jsonNode; + this.status = status; + this.physicalResourceId = physicalResourceId; + this.noEcho = noEcho; + this.reason = reason; } /** @@ -149,6 +160,15 @@ public boolean isNoEcho() { return noEcho; } + /** + * The reason for the failure. + * + * @return a potentially null reason + */ + public String getReason() { + return reason; + } + /** * Includes all Response attributes, including its value in JSON format * @@ -161,6 +181,7 @@ public String toString() { attributes.put("Status", status); attributes.put("PhysicalResourceId", physicalResourceId); attributes.put("NoEcho", noEcho); + attributes.put("Reason", reason); return attributes.entrySet().stream() .map(entry -> entry.getKey() + " = " + entry.getValue()) .collect(Collectors.joining(",", "[", "]")); @@ -182,6 +203,7 @@ public static class Builder { private Status status; private String physicalResourceId; private boolean noEcho; + private String reason; private Builder() { } @@ -263,6 +285,20 @@ public Builder noEcho(boolean noEcho) { return this; } + /** + * Reason for the response. + * Reason is optional for Success responses, but required for Failed responses. + * If not provided it will be replaced with cloudwatch log stream name. + * + * @param reason if null, the default reason will be used + * @return a reference to this builder + */ + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + /** * Builds a Response object for the value. * @@ -277,6 +313,9 @@ public Response build() { node = mapper.valueToTree(value); } Status responseStatus = this.status != null ? this.status : Status.SUCCESS; + if (StringUtils.isNotBlank(this.reason)) { + return new Response(node, responseStatus, physicalResourceId, noEcho, reason); + } return new Response(node, responseStatus, physicalResourceId, noEcho); } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java index 51f0e95f9..938de74d8 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java @@ -324,4 +324,27 @@ void responseBodyStreamFailedResponse() throws Exception { "}"; assertThat(stream.getString()).isEqualTo(expectedJson); } + + @Test + void responseBodyStreamFailedResponseWithReason() throws Exception { + CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); + Context context = mock(Context.class); + CloudFormationResponse cfnResponse = testableCloudFormationResponse(); + String failureReason = "Failed test reason"; + Response failedResponseWithReason = Response.builder(). + status(Response.Status.FAILED).reason(failureReason).build(); + StringInputStream stream = cfnResponse.responseBodyStream(event, context, failedResponseWithReason); + + String expectedJson = "{" + + "\"Status\":\"FAILED\"," + + "\"Reason\":\"" + failureReason + "\"," + + "\"PhysicalResourceId\":null," + + "\"StackId\":null," + + "\"RequestId\":null," + + "\"LogicalResourceId\":null," + + "\"NoEcho\":false," + + "\"Data\":null" + + "}"; + assertThat(stream.getString()).isEqualTo(expectedJson); + } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java index 37fe73d0f..42457d918 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java @@ -33,11 +33,13 @@ void defaultValues() { assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); assertThat(response.getPhysicalResourceId()).isNull(); assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNull(); assertThat(response.toString()).contains("JSON = null"); assertThat(response.toString()).contains("Status = SUCCESS"); assertThat(response.toString()).contains("PhysicalResourceId = null"); assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = null"); } @Test @@ -61,6 +63,27 @@ void explicitNullValues() { assertThat(response.toString()).contains("NoEcho = false"); } + @Test + void explicitReasonWithDefaultValues() { + String reason = "test"; + Response response = Response.builder() + .reason(reason) + .build(); + assertThat(response).isNotNull(); + assertThat(response.getJsonNode()).isNull(); + assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); + assertThat(response.getPhysicalResourceId()).isNull(); + assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNotNull(); + assertThat(response.getReason()).isEqualTo(reason); + + assertThat(response.toString()).contains("JSON = null"); + assertThat(response.toString()).contains("Status = SUCCESS"); + assertThat(response.toString()).contains("PhysicalResourceId = null"); + assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = "+reason); + } + @Test void customNonJsonRelatedValues() { Response response = Response.builder() From 43677d763fd4494a5bedae465619becc188bd9f7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 13:36:56 +0100 Subject: [PATCH 2/8] Bump Kotline example Powertools version to 1.19.0. --- examples/powertools-examples-core/kotlin/build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/powertools-examples-core/kotlin/build.gradle.kts b/examples/powertools-examples-core/kotlin/build.gradle.kts index 170e5b6d8..94dcbe071 100644 --- a/examples/powertools-examples-core/kotlin/build.gradle.kts +++ b/examples/powertools-examples-core/kotlin/build.gradle.kts @@ -14,9 +14,9 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") implementation("com.amazonaws:aws-lambda-java-events:3.11.0") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2") - aspect("software.amazon.lambda:powertools-tracing:1.18.0") - aspect("software.amazon.lambda:powertools-logging:1.18.0") - aspect("software.amazon.lambda:powertools-metrics:1.18.0") + aspect("software.amazon.lambda:powertools-tracing:1.19.0") + aspect("software.amazon.lambda:powertools-logging:1.19.0") + aspect("software.amazon.lambda:powertools-metrics:1.19.0") testImplementation("junit:junit:4.13.2") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") } @@ -36,4 +36,4 @@ tasks.compileTestKotlin { // If using JDK 11 or higher, use the following instead: //kotlin { // jvmToolchain(11) -//} \ No newline at end of file +//} From 83b74f068685f4f290917d04e40e40cfb31da91a Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 13:51:39 +0100 Subject: [PATCH 3/8] Add step to download gradle dynamically. --- .github/workflows/pr_iac_lint.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index b81dcc5eb..d40eb54c9 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -28,6 +28,13 @@ jobs: with: distribution: 'corretto' java-version: 11 + - name: Build Gradle Setup + if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 + working-directory: examples/powertools-examples-core/gradle + run: | + curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) + unzip gradle/wrapper/gradle.zip -d gradle/wrapper/gradle + ./gradle/wrapper/gradle/gradle-8.2.1/bin/gradle wrapper - name: Build Project working-directory: . run: | @@ -53,4 +60,4 @@ jobs: run: | tflint --version tflint --init - tflint -f compact \ No newline at end of file + tflint -f compact From 6fba9e3e2439306935d965bba20e20364c61b83c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 13:55:46 +0100 Subject: [PATCH 4/8] Enable gradle setup step for all Java versions. --- .github/workflows/pr_iac_lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index d40eb54c9..bc16a1c50 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -29,7 +29,6 @@ jobs: distribution: 'corretto' java-version: 11 - name: Build Gradle Setup - if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 working-directory: examples/powertools-examples-core/gradle run: | curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) From f71a52df692df00c17a06ff8f7247a87d29d91fa Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 14:01:00 +0100 Subject: [PATCH 5/8] Use ${{ matrix.project }} for IaC linter workflow. --- .github/workflows/pr_iac_lint.yml | 2 +- .../kotlin/bin/main/helloworld/App.kt | 96 +++++++++++++++++++ .../kotlin/bin/main/helloworld/AppStream.kt | 37 +++++++ .../kotlin/bin/test/helloworld/AppTest.kt | 20 ++++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt create mode 100644 examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt create mode 100644 examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index bc16a1c50..1ae47b0bf 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -29,7 +29,7 @@ jobs: distribution: 'corretto' java-version: 11 - name: Build Gradle Setup - working-directory: examples/powertools-examples-core/gradle + working-directory: examples/powertools-examples-core/${{ matrix.project }} run: | curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) unzip gradle/wrapper/gradle.zip -d gradle/wrapper/gradle diff --git a/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt b/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt new file mode 100644 index 000000000..ed4cf267a --- /dev/null +++ b/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package helloworld + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent +import com.amazonaws.xray.entities.Subsegment +import org.apache.logging.log4j.LogManager +import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger +import software.amazon.cloudwatchlogs.emf.model.DimensionSet +import software.amazon.cloudwatchlogs.emf.model.Unit +import software.amazon.lambda.powertools.logging.Logging +import software.amazon.lambda.powertools.logging.LoggingUtils +import software.amazon.lambda.powertools.metrics.Metrics +import software.amazon.lambda.powertools.metrics.MetricsUtils +import software.amazon.lambda.powertools.tracing.CaptureMode +import software.amazon.lambda.powertools.tracing.Tracing +import software.amazon.lambda.powertools.tracing.TracingUtils +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.net.URL +import java.util.stream.Collectors + +/** + * Handler for requests to Lambda function. + */ + +class App : RequestHandler { + @Logging(logEvent = true, samplingRate = 0.7) + @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + + override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent { + val headers = mapOf("Content-Type" to "application/json", "X-Custom-Header" to "application/json") + MetricsUtils.metricsLogger().putMetric("CustomMetric1", 1.0, Unit.COUNT) + MetricsUtils.withSingleMetric("CustomMetrics2", 1.0, Unit.COUNT, "Another") { metric: MetricsLogger -> + metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")) + metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")) + } + LoggingUtils.appendKey("test", "willBeLogged") + val response = APIGatewayProxyResponseEvent().withHeaders(headers) + return try { + val pageContents = getPageContents("https://checkip.amazonaws.com") + log.info(pageContents) + TracingUtils.putAnnotation("Test", "New") + val output = """ + { + "message": "hello world", + "location": "$pageContents" + } + """.trimIndent() + TracingUtils.withSubsegment("loggingResponse") { _: Subsegment? -> + val sampled = "log something out" + log.info(sampled) + log.info(output) + } + log.info("After output") + response.withStatusCode(200).withBody(output) + } catch (e: RuntimeException) { + response.withBody("{}").withStatusCode(500) + } catch (e: IOException) { + response.withBody("{}").withStatusCode(500) + } + } + + @Tracing + private fun log() { + log.info("inside threaded logging for function") + } + + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) + @Throws(IOException::class) + private fun getPageContents(address: String): String { + val url = URL(address) + TracingUtils.putMetadata("getPageContents", address) + return InputStreamReader(url.openStream()).use { reader -> + reader.readText().trim() + } + } + + private val log = LogManager.getLogger(App::class) +} diff --git a/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt b/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt new file mode 100644 index 000000000..99f6bbfa0 --- /dev/null +++ b/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package helloworld + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestStreamHandler +import com.fasterxml.jackson.databind.ObjectMapper +import software.amazon.lambda.powertools.logging.Logging +import software.amazon.lambda.powertools.metrics.Metrics +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class AppStream : RequestStreamHandler { + @Logging(logEvent = true) + @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @Throws(IOException::class) + override fun handleRequest(input: InputStream, output: OutputStream, context: Context) { + val map: Map<*, *> = mapper.readValue(input, MutableMap::class.java) + println(map.size) + } + + companion object { + private val mapper = ObjectMapper() + } +} diff --git a/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt b/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt new file mode 100644 index 000000000..8aae081e8 --- /dev/null +++ b/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt @@ -0,0 +1,20 @@ +package helloworld + + +import org.junit.Assert +import org.junit.Test + +class AppTest { + @Test + fun successfulResponse() { + val app = App() + val result = app.handleRequest(null, null) + Assert.assertEquals(200, result.statusCode.toLong()) + Assert.assertEquals("application/json", result.headers["Content-Type"]) + val content = result.body + Assert.assertNotNull(content) + Assert.assertTrue(""""message"""" in content) + Assert.assertTrue(""""hello world"""" in content) + Assert.assertTrue(""""location"""" in content) + } +} From 3dd733401e15a2da190d4e9f286ffd4078a7171d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 14:13:23 +0100 Subject: [PATCH 6/8] Revert "Use ${{ matrix.project }} for IaC linter workflow." This reverts commit f71a52df692df00c17a06ff8f7247a87d29d91fa. --- .github/workflows/pr_iac_lint.yml | 2 +- .../kotlin/bin/main/helloworld/App.kt | 96 ------------------- .../kotlin/bin/main/helloworld/AppStream.kt | 37 ------- .../kotlin/bin/test/helloworld/AppTest.kt | 20 ---- 4 files changed, 1 insertion(+), 154 deletions(-) delete mode 100644 examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt delete mode 100644 examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt delete mode 100644 examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index 1ae47b0bf..bc16a1c50 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -29,7 +29,7 @@ jobs: distribution: 'corretto' java-version: 11 - name: Build Gradle Setup - working-directory: examples/powertools-examples-core/${{ matrix.project }} + working-directory: examples/powertools-examples-core/gradle run: | curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) unzip gradle/wrapper/gradle.zip -d gradle/wrapper/gradle diff --git a/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt b/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt deleted file mode 100644 index ed4cf267a..000000000 --- a/examples/powertools-examples-core/kotlin/bin/main/helloworld/App.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package helloworld - -import com.amazonaws.services.lambda.runtime.Context -import com.amazonaws.services.lambda.runtime.RequestHandler -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent -import com.amazonaws.xray.entities.Subsegment -import org.apache.logging.log4j.LogManager -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger -import software.amazon.cloudwatchlogs.emf.model.DimensionSet -import software.amazon.cloudwatchlogs.emf.model.Unit -import software.amazon.lambda.powertools.logging.Logging -import software.amazon.lambda.powertools.logging.LoggingUtils -import software.amazon.lambda.powertools.metrics.Metrics -import software.amazon.lambda.powertools.metrics.MetricsUtils -import software.amazon.lambda.powertools.tracing.CaptureMode -import software.amazon.lambda.powertools.tracing.Tracing -import software.amazon.lambda.powertools.tracing.TracingUtils -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.net.URL -import java.util.stream.Collectors - -/** - * Handler for requests to Lambda function. - */ - -class App : RequestHandler { - @Logging(logEvent = true, samplingRate = 0.7) - @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - - override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent { - val headers = mapOf("Content-Type" to "application/json", "X-Custom-Header" to "application/json") - MetricsUtils.metricsLogger().putMetric("CustomMetric1", 1.0, Unit.COUNT) - MetricsUtils.withSingleMetric("CustomMetrics2", 1.0, Unit.COUNT, "Another") { metric: MetricsLogger -> - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")) - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")) - } - LoggingUtils.appendKey("test", "willBeLogged") - val response = APIGatewayProxyResponseEvent().withHeaders(headers) - return try { - val pageContents = getPageContents("https://checkip.amazonaws.com") - log.info(pageContents) - TracingUtils.putAnnotation("Test", "New") - val output = """ - { - "message": "hello world", - "location": "$pageContents" - } - """.trimIndent() - TracingUtils.withSubsegment("loggingResponse") { _: Subsegment? -> - val sampled = "log something out" - log.info(sampled) - log.info(output) - } - log.info("After output") - response.withStatusCode(200).withBody(output) - } catch (e: RuntimeException) { - response.withBody("{}").withStatusCode(500) - } catch (e: IOException) { - response.withBody("{}").withStatusCode(500) - } - } - - @Tracing - private fun log() { - log.info("inside threaded logging for function") - } - - @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) - @Throws(IOException::class) - private fun getPageContents(address: String): String { - val url = URL(address) - TracingUtils.putMetadata("getPageContents", address) - return InputStreamReader(url.openStream()).use { reader -> - reader.readText().trim() - } - } - - private val log = LogManager.getLogger(App::class) -} diff --git a/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt b/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt deleted file mode 100644 index 99f6bbfa0..000000000 --- a/examples/powertools-examples-core/kotlin/bin/main/helloworld/AppStream.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package helloworld - -import com.amazonaws.services.lambda.runtime.Context -import com.amazonaws.services.lambda.runtime.RequestStreamHandler -import com.fasterxml.jackson.databind.ObjectMapper -import software.amazon.lambda.powertools.logging.Logging -import software.amazon.lambda.powertools.metrics.Metrics -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -class AppStream : RequestStreamHandler { - @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - @Throws(IOException::class) - override fun handleRequest(input: InputStream, output: OutputStream, context: Context) { - val map: Map<*, *> = mapper.readValue(input, MutableMap::class.java) - println(map.size) - } - - companion object { - private val mapper = ObjectMapper() - } -} diff --git a/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt b/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt deleted file mode 100644 index 8aae081e8..000000000 --- a/examples/powertools-examples-core/kotlin/bin/test/helloworld/AppTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package helloworld - - -import org.junit.Assert -import org.junit.Test - -class AppTest { - @Test - fun successfulResponse() { - val app = App() - val result = app.handleRequest(null, null) - Assert.assertEquals(200, result.statusCode.toLong()) - Assert.assertEquals("application/json", result.headers["Content-Type"]) - val content = result.body - Assert.assertNotNull(content) - Assert.assertTrue(""""message"""" in content) - Assert.assertTrue(""""hello world"""" in content) - Assert.assertTrue(""""location"""" in content) - } -} From 1d9bbcca6205d232071122e516fa0b15de69d9e7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 14:13:45 +0100 Subject: [PATCH 7/8] Revert "Enable gradle setup step for all Java versions." This reverts commit 6fba9e3e2439306935d965bba20e20364c61b83c. --- .github/workflows/pr_iac_lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index bc16a1c50..d40eb54c9 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -29,6 +29,7 @@ jobs: distribution: 'corretto' java-version: 11 - name: Build Gradle Setup + if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 working-directory: examples/powertools-examples-core/gradle run: | curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) From 2775ead159ee5f9fc7d62fb162ece40880712ad7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 21 Mar 2025 14:13:47 +0100 Subject: [PATCH 8/8] Revert "Add step to download gradle dynamically." This reverts commit 83b74f068685f4f290917d04e40e40cfb31da91a. --- .github/workflows/pr_iac_lint.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml index d40eb54c9..b81dcc5eb 100644 --- a/.github/workflows/pr_iac_lint.yml +++ b/.github/workflows/pr_iac_lint.yml @@ -28,13 +28,6 @@ jobs: with: distribution: 'corretto' java-version: 11 - - name: Build Gradle Setup - if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 - working-directory: examples/powertools-examples-core/gradle - run: | - curl -L -o gradle/wrapper/gradle.zip https:$(cat gradle/wrapper/gradle-wrapper.properties | grep distributionUrl | cut -d ':' -f 2) - unzip gradle/wrapper/gradle.zip -d gradle/wrapper/gradle - ./gradle/wrapper/gradle/gradle-8.2.1/bin/gradle wrapper - name: Build Project working-directory: . run: | @@ -60,4 +53,4 @@ jobs: run: | tflint --version tflint --init - tflint -f compact + tflint -f compact \ No newline at end of file