From ceb4824086354b6569b8c5871ebb15e56eed8b14 Mon Sep 17 00:00:00 2001 From: Macdonald Date: Mon, 29 Sep 2025 14:18:15 -0400 Subject: [PATCH 1/3] added the Java backend for weathertop2 --- .tools/weathertop/Readme.md | 14 + .tools/weathertop/backend/pom.xml | 211 ++++++++++ .../lambda/HandleEventBridgeInspector.java | 379 ++++++++++++++++++ .../lambda/HandleEventBridgeScheduler.java | 66 +++ .../lambda/HandleGetLogMessage.java | 58 +++ .../lambda/HandleRunFargateTask.java | 111 +++++ .../lambda/HandlerHistoricalSDK.java | 56 +++ .../weathertop/lambda/HandlerQueryFormS3.java | 58 +++ .../com/weathertop/lambda/SDKStatNoTests.java | 65 +++ .../weathertop/lambda/SDKStatsHandler.java | 74 ++++ .../service/EcsEventBridgeInspector.java | 253 ++++++++++++ .../service/EventBridgeScheduler.java | 69 ++++ .../weathertop/service/FargateDeployer.java | 158 ++++++++ .../weathertop/service/FargateTaskRunner.java | 63 +++ .../weathertop/service/GetLogMessages.java | 119 ++++++ .../com/weathertop/service/HistoricalSDK.java | 117 ++++++ .../weathertop/service/QueryLatestFromS3.java | 120 ++++++ .../java/com/weathertop/service/SDKStats.java | 226 +++++++++++ .../backend/src/test/java/WeatheetopTest.java | 58 +++ 19 files changed, 2275 insertions(+) create mode 100644 .tools/weathertop/Readme.md create mode 100644 .tools/weathertop/backend/pom.xml create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeInspector.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeScheduler.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleGetLogMessage.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleRunFargateTask.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerQueryFormS3.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatNoTests.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatsHandler.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/EcsEventBridgeInspector.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/FargateDeployer.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/FargateTaskRunner.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/GetLogMessages.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/HistoricalSDK.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/QueryLatestFromS3.java create mode 100644 .tools/weathertop/backend/src/main/java/com/weathertop/service/SDKStats.java create mode 100644 .tools/weathertop/backend/src/test/java/WeatheetopTest.java diff --git a/.tools/weathertop/Readme.md b/.tools/weathertop/Readme.md new file mode 100644 index 00000000000..5245d2ac4e8 --- /dev/null +++ b/.tools/weathertop/Readme.md @@ -0,0 +1,14 @@ +# Weathertop Test Runner + +## Overview + +Welcome to the Weathertop Test Runner, a comprehensive testing tool designed to validate and ensure the reliability of various AWS code examples. This tool is built to help developers and DevOps engineers verify that their AWS-based applications and services are functioning as expected. + +## Features + +- **Automated Testing**: Run automated tests on AWS code examples to ensure they meet the required standards. +- **Comprehensive Coverage**: Supports a wide range of AWS SDKs. +- **Detailed Reports**: Generates a Dashboard in client app to inform you of the details such as pass rate for the SDKs, the overall number of tests, and so on. + +## Getting Started +ToDO \ No newline at end of file diff --git a/.tools/weathertop/backend/pom.xml b/.tools/weathertop/backend/pom.xml new file mode 100644 index 00000000000..fb5098d0220 --- /dev/null +++ b/.tools/weathertop/backend/pom.xml @@ -0,0 +1,211 @@ + + + 4.0.0 + + org.example + Weathertop + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + 17 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.2 + + false + + + + package + + shade + + + + + + + + + + + + software.amazon.awssdk + bom + 2.29.45 + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.17.0 + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + iam + + + software.amazon.awssdk + cloudwatch + + + software.amazon.awssdk + cloudwatchevents + + + software.amazon.awssdk + cloudwatchlogs + + + software.amazon.awssdk + ecr + + + software.amazon.awssdk + ecs + + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + dynamodb-enhanced + + + software.amazon.awssdk + secretsmanager + + + com.google.code.gson + gson + 2.10.1 + + + com.fasterxml.jackson.core + jackson-core + 2.14.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.14.2 + + + software.amazon.awssdk + kms + + + software.amazon.awssdk + eventbridge + + + + org.mockito + mockito-core + 3.6.0 + test + + + org.mockito + mockito-junit-jupiter + 3.6.0 + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.17.0 + + + com.cronutils + cron-utils + 9.2.0 + + + com.fasterxml.jackson.core + jackson-core + 2.17.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + + software.amazon.awssdk + cloudwatchlogs + + + software.amazon.awssdk + ec2 + + + org.slf4j + slf4j-log4j12 + 2.0.5 + + + software.amazon.awssdk + sso + + + com.amazonaws + aws-lambda-java-core + 1.2.1 + + + software.amazon.awssdk + ssooidc + + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeInspector.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeInspector.java new file mode 100644 index 00000000000..c150f2babd8 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeInspector.java @@ -0,0 +1,379 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.weathertop.service.EcsEventBridgeInspector; +import java.util.Map; + +public class HandleEventBridgeInspector implements RequestHandler { + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody("{\"error\": \"" + message.replace("\"", "\\\"") + "\"}"); + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null || !queryParams.containsKey("language")) { + return createErrorResponse(400, "Missing required query parameter: language"); + } + + String language = queryParams.get("language").toLowerCase(); + logger.log("Received language: " + language); + + switch (language) { + case "java": { + logger.log("Processing Java SDK EventBridge Inspector"); + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + "MyJavaWeathertopCluster", + "WeathertopJava", + "WeathertopJavaLogs", + "ecs-java-schedule" + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "dotnetv4": { + logger.log("Processing .NET EventBridge Inspector"); + + String clusterName = "MyNetWeathertopCluster"; // Must match an actual ECS cluster + String serviceName = "WeathertopNet"; // Must be a service running in that cluster + String logGroup = "WeathertopDotNetLogs"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-dotnet-schedule"; // Must match existing EventBridge rules + + // Call your inspector (assuming your EcsEventBridgeInspector works with these values) + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + serviceName, + logGroup, + eventBridgeRulePrefix + ); + + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + case "dotnetv3": { + logger.log("Processing .NET EventBridge Inspector"); + + String clusterName = "MyNet3WeathertopCluster"; // Must match an actual ECS cluster + String serviceName = "WeathertopNet3"; // Must be a service running in that cluster + String logGroup = "WeathertopDotNet3Logs"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-dotnet3-schedule"; // Must match existing EventBridge rules + + // Call your inspector (assuming your EcsEventBridgeInspector works with these values) + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + serviceName, + logGroup, + eventBridgeRulePrefix + ); + + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "javascriptv3": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyJSWeathertopCluster"; // Must match an actual ECS cluster + String serviceName = "WeathertopJS"; // Must be a service running in that cluster + String logGroup = "WeathertopJSLogs"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-js-schedule"; // Must match existing EventBridge rules + + // Call your inspector (assuming your EcsEventBridgeInspector works with these values) + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + serviceName, + logGroup, + eventBridgeRulePrefix + ); + + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "kotlin": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyKotlinWeathertopCluster"; + String taskDefinitionFamily = "WeathertopKotlin"; + String logGroup = "WeathertopKotlinContainerLogs"; + String ruleName = "ecs-kotlin-schedule"; // Must be a CloudWatch log group + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + + } + + case "python": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyPythonWeathertopCluster"; + String taskDefinitionFamily = "WeathertopPython"; + String logGroup = "WeathertopPythonContainerLogs"; + String ruleName = "ecs-python-schedule"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-python-schedule"; // Must match existing EventBridge rules + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "php": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyPHPWeathertopCluster"; + String taskDefinitionFamily = "WeathertopPhp"; + String logGroup = "WeathertopPhpContainerLogs"; + String ruleName = "ecs-php-schedule"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-js-schedule"; // Must match existing EventBridge rules + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "gov2": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyGoWeathertopCluster"; + String taskDefinitionFamily = "WeathertopGo"; + String logGroup = "WeathertopGoContainerLogs"; + String ruleName = "ecs-go-schedule"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-js-schedule"; // Must match existing EventBridge rules + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "rustv1": { + logger.log("Processing RUST EventBridge Inspector"); + + String clusterName = "MyRustWeathertopCluster"; + String taskDefinitionFamily = "WeathertopRust"; + String logGroup = "WeathertopRustContainerLogs"; + String ruleName = "ecs-rust-schedule"; // Must be a CloudWatch log group + String eventBridgeRulePrefix = "ecs-rust-schedule"; // Must match existing EventBridge rules + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "cpp": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyCPPWeathertopCluster"; + String taskDefinitionFamily = "WeathertopCPP"; + String logGroup = "WeathertopCPPContainerLogs"; + String ruleName = "ecs-cpp-schedule"; // Must be a CloudWatch log group + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + case "ruby": { + logger.log("Processing JS EventBridge Inspector"); + + String clusterName = "MyRubyWeathertopCluster"; + String taskDefinitionFamily = "WeathertopRuby"; + String logGroup = "WeathertopRubyContainerLogs"; + String ruleName = "ecs-ruby-schedule"; // Must be a CloudWatch log group + + EcsEventBridgeInspector inspector = new EcsEventBridgeInspector( + clusterName, + taskDefinitionFamily, + logGroup, + ruleName + ); + + Map data = inspector.inspect(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(data); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + } + + + default: + String unknownMsg = "Unknown language parameter: '" + language + "'"; + logger.log(unknownMsg); + return createErrorResponse(400, unknownMsg); + } + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeScheduler.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeScheduler.java new file mode 100644 index 00000000000..25ef3dad681 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleEventBridgeScheduler.java @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.EventBridgeScheduler; +import java.util.Map; + +public class HandleEventBridgeScheduler implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + EventBridgeScheduler schedule = new EventBridgeScheduler(); + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null ) { + return createErrorResponse(400, "Missing required query parameters"); + } + + String taskDefinitionArnVal = queryParams.get("taskDefinitionArnVal").toLowerCase(); // Normalize + String clusterName = queryParams.get("clusterName").toLowerCase(); + String cron = queryParams.get("cron").toLowerCase(); + String ruleName = ""; + if (clusterName.equals("MyNetWeathertopCluster")) { + ruleName = "ecs-dotnet-schedule"; + } else if (clusterName.equals("MyJavaWeathertopCluster")) { + ruleName = "ecs-java-schedule"; + + } else if (clusterName.equals("MyRustWeathertopCluster")) { + ruleName = "ecs-rust-schedule"; + } else { + ruleName = "ecs-java-schedule"; + } + + String message = schedule.setScheduler(taskDefinitionArnVal, clusterName, cron, ruleName); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(message); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} \ No newline at end of file diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleGetLogMessage.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleGetLogMessage.java new file mode 100644 index 00000000000..1da6087422c --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleGetLogMessage.java @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.GetLogMessages; +import java.util.Map; + +public class HandleGetLogMessage implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null || !queryParams.containsKey("language")) { + return createErrorResponse(400, "Missing required query parameter: language"); + } + + String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received language: " + language); + + // Fetch latest JSON from S3 + GetLogMessages logs = new GetLogMessages(); + String json = logs.getHistoricalSummary(language); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleRunFargateTask.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleRunFargateTask.java new file mode 100644 index 00000000000..6a2c63588ed --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandleRunFargateTask.java @@ -0,0 +1,111 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.FargateTaskRunner; +import java.util.Map; + +public class HandleRunFargateTask implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null || !queryParams.containsKey("language")) { + return createErrorResponse(400, "Missing required query parameter: language"); + } + + String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received language: " + language); + + FargateTaskRunner runner = new FargateTaskRunner(); + String taskRunId = ""; + if (language.compareTo("java") == 0) { + String clusteerName = "MyJavaWeathertopCluster"; + String defName = "WeathertopJava"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("rustv1") == 0) { + String clusteerName = "MyRustWeathertopCluster"; + String defName = "WeathertopRust"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("ruby") == 0) { + String clusteerName = "MyRubyWeathertopCluster"; + String defName = "WeathertopRuby"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("kotlin") == 0) { + String clusteerName = "MyKotlinWeathertopCluster"; + String defName = "WeathertopKotlin"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("dotnetv4") == 0) { + String clusteerName = "MyNetWeathertopCluster"; + String defName = "WeathertopNet"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("dotnetv3") == 0) { + String clusteerName = "MyNet3WeathertopCluster"; + String defName = "WeathertopNet3"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("javascriptv3") == 0) { + + String clusteerName = "MyJSWeathertopCluster"; + String defName = "WeathertopJS"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else if (language.compareTo("php") == 0) { + String clusterName = "MyPHPWeathertopCluster"; + String defName = "WeathertopPhp"; // Your PHP task definition family name + taskRunId = runner.runFargateTask(defName, clusterName); + + } else if (language.compareTo("python") == 0) { + String clusterName = "MyPythonWeathertopCluster"; + String defName = "WeathertopPython"; // Your PHP task definition family name + taskRunId = runner.runFargateTask(defName, clusterName); + + } else if (language.compareTo("gov2") == 0) { + String clusteerName = "MyGoWeathertopCluster"; + String defName = "WeathertopGo"; + taskRunId = runner.runFargateTask(defName, clusteerName); + + } else { + logger.log("Unsupported language: " + language); + // Optionally handle default case + } + + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody("{\"taskArn\": \"" + taskRunId.replace("\"", "\\\"") + "\"}"); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} \ No newline at end of file diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java new file mode 100644 index 00000000000..6ae0aa281b1 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java @@ -0,0 +1,56 @@ +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.HistoricalSDK; + + +import java.util.Map; + +public class HandlerHistoricalSDK implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null || !queryParams.containsKey("language")) { + return createErrorResponse(400, "Missing required query parameter: language"); + } + + String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received language: " + language); + + HistoricalSDK history = new HistoricalSDK(); + String json = history.getHistoricalSummary(language); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerQueryFormS3.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerQueryFormS3.java new file mode 100644 index 00000000000..c1235e90adc --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerQueryFormS3.java @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.QueryLatestFromS3; +import java.util.Map; + +public class HandlerQueryFormS3 implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + if (queryParams == null || !queryParams.containsKey("language")) { + return createErrorResponse(400, "Missing required query parameter: language"); + } + + String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received language: " + language); + + // Fetch latest JSON from S3 + QueryLatestFromS3 fetcher = new QueryLatestFromS3(); + String json = fetcher.readJsonFromS3(language); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(json); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatNoTests.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatNoTests.java new file mode 100644 index 00000000000..849076db018 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatNoTests.java @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.SDKStats; +import java.util.Map; + +public class SDKStatNoTests implements RequestHandler { + + /** + * Handles the incoming API Gateway request to get the latest summaries for + * a set of SDK languages for the Coverage by Language component. + * + * @param event the {@link APIGatewayProxyRequestEvent} containing the request details + * @param context the {@link Context} object providing information about the current execution environment + * @return a {@link APIGatewayProxyResponseEvent} containing the response to be returned to the API Gateway + */ + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + + // Test with hard coding lanhs + String[] langs = {"dotnetv4", "dotnetv3", "php", "python", "ruby", "javascriptv3"}; + // String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received languages: " + langs); + + // Fetch latest JSON from S3 + SDKStats langStats = new SDKStats(); + String JSON = langStats.getNoTestsBySDK(langs); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(JSON); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatsHandler.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatsHandler.java new file mode 100644 index 00000000000..d044204744f --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/SDKStatsHandler.java @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +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.weathertop.service.SDKStats; +import java.util.Map; + +/** + * The {@code SDKStatsHandler} class is an implementation of the {@link RequestHandler} interface, which is responsible + * for handling API Gateway proxy requests and generating the corresponding responses. + * + *

This class retrieves the language test statistics from the {@link SDKStats} class, converts them to a JSON + * string, logs the JSON data, and returns an {@link APIGatewayProxyResponseEvent} with a status code of 200 and the + * JSON data as the response body.

+ */ + +public class SDKStatsHandler implements RequestHandler { + + /** + * Handles the incoming API Gateway request to get the latest summaries for + * a set of SDK languages for the Coverage by Language component. + * + * @param event the {@link APIGatewayProxyRequestEvent} containing the request details + * @param context the {@link Context} object providing information about the current execution environment + * @return a {@link APIGatewayProxyResponseEvent} containing the response to be returned to the API Gateway + */ + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + try { + // Extract "language" from query parameters + Map queryParams = event.getQueryStringParameters(); + + // Test with hard coding lanhs + String[] langs = {"java", "kotlin", "dotnetv4", "dotnetv3", "php", "javascriptv3", "python", "gov2", "rustv1", "cpp", "ruby"}; + // String language = queryParams.get("language").toLowerCase(); // Normalize + logger.log("Received languages: " + langs); + + // Fetch latest JSON from S3 + SDKStats langStats = new SDKStats(); + String JSON = langStats.getCoverageSummary(langs); + + // Return response + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of( + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET,OPTIONS", + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token" + )) + .withBody(JSON); + + } catch (Exception e) { + logger.log("Exception: " + e.getMessage()); + return createErrorResponse(500, "Internal server error: " + e.getMessage()); + } + } + + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Access-Control-Allow-Origin", "*")) + .withBody("{\"error\": \"" + message + "\"}"); + } +} + + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/EcsEventBridgeInspector.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EcsEventBridgeInspector.java new file mode 100644 index 00000000000..632085dc5dd --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EcsEventBridgeInspector.java @@ -0,0 +1,253 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import software.amazon.awssdk.services.ec2.model.NetworkInterface; +import software.amazon.awssdk.services.ecs.EcsClient; +import software.amazon.awssdk.services.ecs.model.*; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.*; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.eventbridge.model.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import software.amazon.awssdk.services.eventbridge.model.AwsVpcConfiguration; +import java.util.*; +import java.util.stream.Collectors; + +public class EcsEventBridgeInspector { + private final String clusterName; + private final String taskDefinitionFamily; + private final String logGroup; + private final String ruleName; // changed from rulePrefix + + private final EcsClient ecs = EcsClient.create(); + private final Ec2Client ec2 = Ec2Client.create(); + private final EventBridgeClient events = EventBridgeClient.create(); + + public EcsEventBridgeInspector(String clusterName, String taskDefinitionFamily, String logGroup, String ruleName) { + this.clusterName = clusterName; + this.taskDefinitionFamily = taskDefinitionFamily; + this.logGroup = logGroup; + this.ruleName = ruleName; + } + + /** + * Inspects various components related to ECS (Elastic Container Service) and EventBridge. + * This method performs the following operations: + * + * 1. Lists the task definitions for a given family, retrieves the latest one, and fetches its details. + * 2. Describes the ECS cluster specified by the cluster name. + * 3. Lists and describes the running tasks for the specified task definition family within the cluster. + * 4. Retrieves details about a specific EventBridge rule, including its targets and ECS parameters if applicable. + * + * The method returns a Map containing the following keys: + * - "latestTaskDefinitionArn": The ARN of the latest task definition. + * - "taskDefinitionMetadata": A Map containing metadata about the task definition, including taskRoleArn, executionRoleArn, networkMode, cpu, and memory. + * - "clusterArn": The ARN of the ECS cluster. + * - "tasks": A List of Maps, each representing a running task with details like taskArn, lastStatus, desiredStatus, and networkInterface information. + * - "taskOrService": A String indicating whether the running task is a standalone Task or part of a Service. + * - "eventBridgeRule": A Map containing details about the EventBridge rule, including its name, scheduleExpression, state, description, human-readable schedule, and a list of targets with their ECS parameters if applicable. + * + * In case of errors (e.g., no task definitions found, cluster not found, unable to retrieve EventBridge rule), + * the response Map will contain an "error" key with the corresponding error message. + * + * @return A Map containing the inspection results and metadata. + */ + public Map inspect() { + Map response = new LinkedHashMap<>(); + + // List task definitions, get latest + ListTaskDefinitionsResponse defsResp = ecs.listTaskDefinitions(ListTaskDefinitionsRequest.builder() + .familyPrefix(taskDefinitionFamily) + .sort(SortOrder.DESC) + .maxResults(1) + .build()); + + List arns = defsResp.taskDefinitionArns(); + if (arns.isEmpty()) { + response.put("error", "No task definitions found."); + return response; + } + + String latestArn = arns.get(0); + response.put("latestTaskDefinitionArn", latestArn); + + DescribeTaskDefinitionResponse defDetail = ecs.describeTaskDefinition( + DescribeTaskDefinitionRequest.builder() + .taskDefinition(latestArn).build()); + + TaskDefinition td = defDetail.taskDefinition(); + Map taskDefData = new LinkedHashMap<>(); + taskDefData.put("taskRoleArn", td.taskRoleArn()); + taskDefData.put("executionRoleArn", td.executionRoleArn()); + taskDefData.put("networkMode", td.networkModeAsString()); + taskDefData.put("cpu", td.cpu()); + taskDefData.put("memory", td.memory()); + response.put("taskDefinitionMetadata", taskDefData); + + // Describe ECS cluster + DescribeClustersResponse clusResp = ecs.describeClusters( + DescribeClustersRequest.builder().clusters(clusterName).build()); + if (clusResp.clusters().isEmpty()) { + response.put("error", "Cluster not found: " + clusterName); + return response; + } + + String clusterArn = clusResp.clusters().get(0).clusterArn(); + response.put("clusterArn", clusterArn); + + // List running tasks for the family + ListTasksResponse tasksResp = ecs.listTasks(ListTasksRequest.builder() + .cluster(clusterName) + .family(taskDefinitionFamily) + .desiredStatus(DesiredStatus.RUNNING) + .maxResults(5) + .build()); + + List> tasksList = new ArrayList<>(); + String taskOrServiceType = "Unknown"; + + if (tasksResp.taskArns().isEmpty()) { + response.put("tasks", tasksList); + response.put("message", "No RUNNING ECS tasks found."); + } else { + DescribeTasksResponse dtResp = ecs.describeTasks( + DescribeTasksRequest.builder() + .cluster(clusterName) + .tasks(tasksResp.taskArns()) + .build()); + + for (Task t : dtResp.tasks()) { + Map taskData = new LinkedHashMap<>(); + taskData.put("taskArn", t.taskArn()); + taskData.put("lastStatus", t.lastStatus()); + taskData.put("desiredStatus", t.desiredStatus()); + + // Detect type (Task or Service) + String group = t.group(); + if (group != null && group.startsWith("service:")) { + taskOrServiceType = "Service"; + } else { + taskOrServiceType = "Task"; + } + + if ("RUNNING".equalsIgnoreCase(t.lastStatus())) { + Optional eniIdOpt = t.attachments().stream() + .filter(a -> "ElasticNetworkInterface".equals(a.type())) + .flatMap(a -> a.details().stream()) + .filter(d -> "networkInterfaceId".equals(d.name())) + .map(d -> d.value()) + .findFirst(); + + if (eniIdOpt.isPresent()) { + String eniId = eniIdOpt.get(); + try { + DescribeNetworkInterfacesResponse eniResp = ec2.describeNetworkInterfaces( + DescribeNetworkInterfacesRequest.builder() + .networkInterfaceIds(eniId).build()); + if (!eniResp.networkInterfaces().isEmpty()) { + NetworkInterface eni = eniResp.networkInterfaces().get(0); + Map eniData = new LinkedHashMap<>(); + eniData.put("eniId", eni.networkInterfaceId()); + eniData.put("subnetId", eni.subnetId()); + eniData.put("securityGroups", eni.groups().stream() + .map(GroupIdentifier::groupId) + .collect(Collectors.toList())); + eniData.put("vpcId", eni.vpcId()); + taskData.put("networkInterface", eniData); + } else { + taskData.put("networkInterface", "No network interfaces found for ENI ID: " + eniId); + } + } catch (Exception e) { + taskData.put("networkInterfaceError", "Could not retrieve ENI '" + eniId + "': " + e.getMessage()); + } + } else { + taskData.put("networkInterface", "No ENI found on the running task."); + } + } else { + taskData.put("networkInterface", "Skipped because task is not RUNNING."); + } + + tasksList.add(taskData); + } + + response.put("tasks", tasksList); + } + + response.put("taskOrService", taskOrServiceType); + + // --- EventBridge rule by specific name --- + Map ruleData = new LinkedHashMap<>(); + try { + DescribeRuleResponse ruleResp = events.describeRule( + DescribeRuleRequest.builder() + .name(ruleName) + .build()); + + ruleData.put("name", ruleResp.name()); + ruleData.put("scheduleExpression", ruleResp.scheduleExpression()); + ruleData.put("state", ruleResp.stateAsString()); + ruleData.put("description", ruleResp.description()); + ruleData.put("humanReadableSchedule", humanTime(ruleResp.scheduleExpression())); + + ListTargetsByRuleResponse targetsResp = events.listTargetsByRule( + ListTargetsByRuleRequest.builder() + .rule(ruleName) + .build()); + + List> targetsList = new ArrayList<>(); + for (Target target : targetsResp.targets()) { + Map targetData = new LinkedHashMap<>(); + targetData.put("id", target.id()); + targetData.put("arn", target.arn()); + + if (target.ecsParameters() != null) { + EcsParameters p = target.ecsParameters(); + Map ecsParams = new LinkedHashMap<>(); + ecsParams.put("taskDefinitionArn", p.taskDefinitionArn()); + ecsParams.put("launchType", p.launchTypeAsString()); + + if (p.networkConfiguration() != null && p.networkConfiguration().awsvpcConfiguration() != null) { + AwsVpcConfiguration awsvpc = p.networkConfiguration().awsvpcConfiguration(); + ecsParams.put("subnets", awsvpc.subnets()); + ecsParams.put("securityGroups", awsvpc.securityGroups()); + ecsParams.put("assignPublicIp", awsvpc.assignPublicIpAsString()); + } + targetData.put("ecsParameters", ecsParams); + } + targetsList.add(targetData); + } + ruleData.put("targets", targetsList); + + } catch (EventBridgeException e) { + ruleData.put("error", "Could not retrieve rule '" + ruleName + "': " + e.getMessage()); + } + + response.put("eventBridgeRule", ruleData); + + return response; + } + + private String humanTime(String expr) { + if (expr != null && expr.startsWith("cron(") && expr.endsWith(")")) { + String[] parts = expr.substring(5, expr.length() - 1).split(" "); + Map map = new HashMap<>(); + map.put("1", "Sunday"); + map.put("2", "Monday"); + map.put("3", "Tuesday"); + map.put("4", "Wednesday"); + map.put("5", "Thursday"); + map.put("6", "Friday"); + map.put("7", "Saturday"); + String dow = map.getOrDefault(parts[4], parts[4]); + int hr = Integer.parseInt(parts[1]); + return "Every " + dow + " at " + (hr % 12 == 0 ? 12 : hr % 12) + ":00 " + (hr < 12 ? "AM" : "PM") + " UTC"; + } else if (expr != null && expr.startsWith("rate(")) { + return "Rate schedule: " + expr.substring(5, expr.length() - 1); + } + return "Schedule: " + expr; + } +} + diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java new file mode 100644 index 00000000000..af0611a863e --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java @@ -0,0 +1,69 @@ +package com.weathertop.service; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.eventbridge.model.AssignPublicIp; +import software.amazon.awssdk.services.eventbridge.model.AwsVpcConfiguration; +import software.amazon.awssdk.services.eventbridge.model.EcsParameters; +import software.amazon.awssdk.services.eventbridge.model.LaunchType; +import software.amazon.awssdk.services.eventbridge.model.NetworkConfiguration; +import software.amazon.awssdk.services.eventbridge.model.PutRuleRequest; +import software.amazon.awssdk.services.eventbridge.model.PutTargetsRequest; +import software.amazon.awssdk.services.eventbridge.model.RuleState; +import software.amazon.awssdk.services.eventbridge.model.Target; + +public class EventBridgeScheduler { + + /** + * Schedules an ECS task using EventBridge. + * + * @param taskDefinitionArnVal Full ARN of the ECS task definition (e.g., arn:aws:ecs:us-east-1:123456789012:task-definition/MyTask:1) + * @param clusterName Name of the ECS cluster (e.g., MyCluster) + * @param cron Valid EventBridge cron expression (e.g., "cron(0 0 ? * 1 *)") + */ + public String setScheduler(String taskDefinitionArnVal, String clusterName, String cron, String ruleName) { + Region region = Region.US_EAST_1; + EventBridgeClient eventBrClient = EventBridgeClient.builder() + .region(region) + .build(); + + String roleArn = "arn:aws:iam::814548047983:role/EcsEventsRole"; + + // Step 1: Create am EventBridge Rule + PutRuleRequest ruleRequest = PutRuleRequest.builder() + .name(ruleName) + .scheduleExpression(cron) + .state(RuleState.ENABLED) + .build(); + + eventBrClient.putRule(ruleRequest); + + // Step 2: Define ECS Task Target + Target target = Target.builder() + .id("ecs-target") + .arn("arn:aws:ecs:us-east-1:814548047983:cluster/" + clusterName) + .roleArn(roleArn) + .ecsParameters(EcsParameters.builder() + .taskDefinitionArn(taskDefinitionArnVal) + .launchType(LaunchType.FARGATE) + .networkConfiguration(NetworkConfiguration.builder() + .awsvpcConfiguration(AwsVpcConfiguration.builder() + .subnets("subnet-03c28397a3a7cd314") + .securityGroups("sg-0e357c99b6b13bf62") + .assignPublicIp(AssignPublicIp.ENABLED) + .build()) + .build()) + .build()) + .build(); + + // Step 3: Attach ECS Task Target + PutTargetsRequest targetsRequest = PutTargetsRequest.builder() + .rule(ruleName) + .targets(target) + .build(); + + eventBrClient.putTargets(targetsRequest); + String msg = "✅ EventBridge rule set for: " + cron; + return msg; + } +} \ No newline at end of file diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateDeployer.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateDeployer.java new file mode 100644 index 00000000000..d7596f2d2f6 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateDeployer.java @@ -0,0 +1,158 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import software.amazon.awssdk.services.ecs.EcsClient; +import software.amazon.awssdk.services.ecs.model.*; +import software.amazon.awssdk.services.iam.IamClient; +import software.amazon.awssdk.services.iam.model.*; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.*; +import java.util.Map; + +public class FargateDeployer { + + private static final String AWS_ACCOUNT_ID = "814548047983"; + private static final String AWS_REGION = "us-east-1"; + private static final String CLUSTER_NAME = "MyJavaWeathertopCluster"; + private static final String TASK_DEF_NAME = "WeathertopJava"; + private static final String SERVICE_NAME = "JavaWeathertop"; + private static final String CONTAINER_NAME = "weathertop-java"; + private static final String IMAGE = AWS_ACCOUNT_ID + ".dkr.ecr." + AWS_REGION + ".amazonaws.com/weathertop-java:latest"; + private static final String LOG_GROUP = "WeathertopContainerLogs"; + private static final String ROLE_NAME = "ecsTaskExecutionRole"; + private static final String[] SUBNETS = { "subnet-87bd1a89", "subnet-ef28c6b0" }; + private static final String[] SECURITY_GROUPS = { "sg-0e357c99b6b13bf62" }; + + private final EcsClient ecsClient; + private final IamClient iamClient; + private final CloudWatchLogsClient logsClient; + + public FargateDeployer() { + ecsClient = EcsClient.builder().region(software.amazon.awssdk.regions.Region.of(AWS_REGION)).build(); + iamClient = IamClient.builder().region(software.amazon.awssdk.regions.Region.AWS_GLOBAL).build(); + logsClient = CloudWatchLogsClient.builder().region(software.amazon.awssdk.regions.Region.of(AWS_REGION)).build(); + } + + public void deploy() { + ensureIamRole(); + String clusterArn = ensureCluster(); + ensureLogGroup(); + String taskDefArn = registerTaskDefinition(); + createOrUpdateService(clusterArn, taskDefArn); + System.out.println("✅ ECS Fargate deployment complete."); + } + + private void ensureIamRole() { + try { + iamClient.getRole(GetRoleRequest.builder().roleName(ROLE_NAME).build()); + System.out.println("IAM Role exists: " + ROLE_NAME); + } catch (NoSuchEntityException e) { + System.out.println("Creating IAM Role: " + ROLE_NAME); + String assumeRolePolicy = "{ \"Version\": \"2012-10-17\", \"Statement\": [{ \"Effect\": \"Allow\", \"Principal\": { \"Service\": \"ecs-tasks.amazonaws.com\" }, \"Action\": \"sts:AssumeRole\" }] }"; + iamClient.createRole(CreateRoleRequest.builder() + .roleName(ROLE_NAME) + .assumeRolePolicyDocument(assumeRolePolicy) + .description("ECS Task Execution Role for Fargate") + .build()); + iamClient.attachRolePolicy(AttachRolePolicyRequest.builder() + .roleName(ROLE_NAME) + .policyArn("arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") + .build()); + } + } + + private String ensureCluster() { + DescribeClustersResponse desc = ecsClient.describeClusters(DescribeClustersRequest.builder().clusters(CLUSTER_NAME).build()); + if (!desc.clusters().isEmpty() && desc.clusters().get(0).status().equals("ACTIVE")) { + System.out.println("Cluster exists: " + CLUSTER_NAME); + return desc.clusters().get(0).clusterArn(); + } + CreateClusterResponse createResp = ecsClient.createCluster(CreateClusterRequest.builder().clusterName(CLUSTER_NAME).build()); + System.out.println("Created cluster: " + createResp.cluster().clusterArn()); + return createResp.cluster().clusterArn(); + } + + private void ensureLogGroup() { + try { + logsClient.describeLogGroups(DescribeLogGroupsRequest.builder().logGroupNamePrefix(LOG_GROUP).build()) + .logGroups().stream() + .filter(g -> g.logGroupName().equals(LOG_GROUP)) + .findAny() + .orElseThrow(() -> new RuntimeException("Log group not found")); + + System.out.println("Log group exists: " + LOG_GROUP); + } catch (RuntimeException e) { + System.out.println("Creating log group: " + LOG_GROUP); + logsClient.createLogGroup(CreateLogGroupRequest.builder().logGroupName(LOG_GROUP).build()); + } + } + + private String registerTaskDefinition() { + RegisterTaskDefinitionResponse resp = ecsClient.registerTaskDefinition(RegisterTaskDefinitionRequest.builder() + .family(TASK_DEF_NAME) + .networkMode(NetworkMode.AWSVPC) + .requiresCompatibilities(Compatibility.FARGATE) + .cpu("256") + .memory("512") + .executionRoleArn("arn:aws:iam::" + AWS_ACCOUNT_ID + ":role/" + ROLE_NAME) + .containerDefinitions(ContainerDefinition.builder() + .name(CONTAINER_NAME) + .image(IMAGE) + .cpu(256) + .memory(512) + .essential(true) + .logConfiguration(LogConfiguration.builder() + .logDriver(LogDriver.AWSLOGS) + .options( + Map.of( + "awslogs-group", LOG_GROUP, + "awslogs-region", AWS_REGION, + "awslogs-stream-prefix", "ecs" + ) + ).build()) + .build()) + .build()); + System.out.println("Registered task definition: " + resp.taskDefinition().taskDefinitionArn()); + return resp.taskDefinition().taskDefinitionArn(); + } + + private void createOrUpdateService(String clusterArn, String taskDefArn) { + try { + DescribeServicesResponse desc = ecsClient.describeServices(DescribeServicesRequest.builder() + .cluster(CLUSTER_NAME) + .services(SERVICE_NAME) + .build()); + + if (!desc.services().isEmpty() && desc.services().get(0).status().equals("ACTIVE")) { + System.out.println("Service exists, updating..."); + ecsClient.updateService(UpdateServiceRequest.builder() + .cluster(CLUSTER_NAME) + .service(SERVICE_NAME) + .taskDefinition(taskDefArn) + .desiredCount(1) + .build()); + return; + } + } catch (Exception e) { + // Service does not exist or error, proceed to create + } + + System.out.println("Creating new service..."); + ecsClient.createService(CreateServiceRequest.builder() + .cluster(CLUSTER_NAME) + .serviceName(SERVICE_NAME) + .taskDefinition(taskDefArn) + .desiredCount(1) + .launchType(LaunchType.FARGATE) + .networkConfiguration(NetworkConfiguration.builder() + .awsvpcConfiguration(AwsVpcConfiguration.builder() + .subnets(SUBNETS) + .securityGroups(SECURITY_GROUPS) + .assignPublicIp(AssignPublicIp.ENABLED) + .build()) + .build()) + .build()); + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateTaskRunner.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateTaskRunner.java new file mode 100644 index 00000000000..f2b277f2c3b --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/FargateTaskRunner.java @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ecs.EcsClient; +import software.amazon.awssdk.services.ecs.model.*; +import java.util.List; + +public class FargateTaskRunner { + + private static final Region REGION = Region.US_EAST_1; + + // Network config matching Python exactly + private static final List SUBNETS = List.of( + "subnet-03c28397a3a7cd314", + "subnet-06dde61595900f899" + ); + + private static final List SECURITY_GROUPS = List.of( + "sg-0e357c99b6b13bf62" + ); + + /** + * Runs a Fargate task using the specified task definition in Amazon ECS. + * + * @param taskDefinition The ARN or family name of the task definition to use. + * @return The ARN of the task that was started, or null if the task failed to start. + */ + public String runFargateTask(String taskDefinition, String clusterName ) { + EcsClient ecsClient = EcsClient.builder().region(REGION).build(); + + AwsVpcConfiguration vpcConfig = AwsVpcConfiguration.builder() + .subnets(SUBNETS) + .securityGroups(SECURITY_GROUPS) + .assignPublicIp(AssignPublicIp.ENABLED) + .build(); + + NetworkConfiguration netConfig = NetworkConfiguration.builder() + .awsvpcConfiguration(vpcConfig) + .build(); + + RunTaskRequest request = RunTaskRequest.builder() + .cluster(clusterName) + .launchType(LaunchType.FARGATE) + .taskDefinition(taskDefinition) + .networkConfiguration(netConfig) + .count(1) + .build(); + + RunTaskResponse response = ecsClient.runTask(request); + + if (!response.tasks().isEmpty()) { + String taskArn = response.tasks().get(0).taskArn(); + System.out.println("🚀 Started task: " + taskArn); + return taskArn; + } else { + System.err.println("❌ Run task failed: " + response.failures()); + return null; + } + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/GetLogMessages.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/GetLogMessages.java new file mode 100644 index 00000000000..0f71c21c4c6 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/GetLogMessages.java @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Object; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GetLogMessages { + + private static final Region REGION = Region.US_EAST_1; + private static final String BUCKET_NAME = "weathertop2"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm"); + + + /** + * Retrieves the historical test summary for a specific SDK language from S3. + *

+ * This method performs the following steps: + *

    + *
  1. Builds an {@link S3Client} for the configured {@code REGION}.
  2. + *
  3. Finds the latest JSON test result file in the bucket {@code BUCKET_NAME} for the given language using {@link #getLatestFileKey}.
  4. + *
  5. Downloads the JSON file as bytes and converts it to a UTF-8 string.
  6. + *
  7. Parses the JSON and extracts the {@code tests} array under {@code results}.
  8. + *
  9. Returns the extracted tests array as a pretty-printed JSON string. If the {@code tests} node is not an array, returns {@code "[]"}
  10. + *
+ *

+ * Example output: + * + * @param language The SDK language prefix (e.g., "java", "python") used to locate the latest test results. + * @return A pretty-printed JSON string representing the {@code tests} array for the specified language. + * @throws RuntimeException if there is a failure reading the JSON, extracting the tests node, or any S3-related error occurs. + */ + public String getHistoricalSummary(String language) { + S3Client s3 = S3Client.builder() + .region(REGION) + .build(); + + String latestFileKey = getLatestFileKey(s3, BUCKET_NAME, language); + System.out.println("Latest file: " + latestFileKey); + + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(BUCKET_NAME) + .key(latestFileKey) + .build(); + + ResponseBytes objectBytes = s3.getObjectAsBytes(getRequest); + String jsonString = objectBytes.asString(StandardCharsets.UTF_8); + + try { + JsonNode root = MAPPER.readTree(jsonString); + JsonNode testsNode = root.path("results").path("tests"); + + if (!testsNode.isArray()) { + return "[]"; + } + + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(testsNode); + } catch (Exception e) { + throw new RuntimeException("Failed to parse JSON or extract tests node", e); + } + } + + + /** + * Retrieves the key of the latest timestamped JSON file for a given SDK language in an S3 bucket. + * Example filename: {@code java-2025-07-10T12-15.json} + * If multiple matching files exist, the one with the most recent timestamp is returned. + * + * @param s3Client The {@link S3Client} used to interact with Amazon S3. + * @param bucketName The name of the S3 bucket to search for JSON files. + * @param languagePrefix The prefix for the SDK language (e.g., "java", "python") used to filter files. + * @return The key of the latest JSON file matching the specified language prefix. + * @throws RuntimeException if no matching files are found in the bucket. + */ + public static String getLatestFileKey(S3Client s3Client, String bucketName, String languagePrefix) { + Pattern filePattern = Pattern.compile(languagePrefix + "-(\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2})\\.json"); + + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucketName) + .prefix(languagePrefix + "-") + .build(); + + ListObjectsV2Response response = s3Client.listObjectsV2(request); + List objects = response.contents(); + + return objects.stream() + .map(S3Object::key) + .filter(key -> filePattern.matcher(key).matches()) + .sorted(Comparator.comparing((String key) -> extractTimestamp(key, filePattern)).reversed()) + .findFirst() + .orElseThrow(() -> new RuntimeException("No matching files found for prefix: " + languagePrefix)); + } + + private static LocalDateTime extractTimestamp(String key, Pattern pattern) { + Matcher matcher = pattern.matcher(key); + if (matcher.find()) { + return LocalDateTime.parse(matcher.group(1), FORMATTER); + } else { + throw new RuntimeException("Invalid file format: " + key); + } + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/HistoricalSDK.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/HistoricalSDK.java new file mode 100644 index 00000000000..39a4f3f9941 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/HistoricalSDK.java @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HistoricalSDK { + + private static final Region REGION = Region.US_EAST_1; + private static final String BUCKET_NAME = "weathertop2"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final DateTimeFormatter FILENAME_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * Returns historical test summary for the latest 3 runs of the given language. + * + * @param language SDK language prefix (e.g., "java") + * @return A JSON string with the historical summary in the required format + */ + public String getHistoricalSummary(String language) { + S3Client s3 = S3Client.builder().region(REGION).build(); + String prefix = language + "-"; + Pattern pattern = Pattern.compile(language + "-(\\d{4}-\\d{2}-\\d{2})T\\d{2}-\\d{2}\\.json"); + + // List all matching files + List matchedObjects = listMatchingObjects(s3, BUCKET_NAME, prefix, pattern); + + // Sort descending by date in filename + matchedObjects.sort(Comparator.comparing( + o -> extractDateFromKey(o.key(), pattern), Comparator.reverseOrder())); + + List> historyList = new ArrayList<>(); + + for (int i = 0; i < Math.min(3, matchedObjects.size()); i++) { + String key = matchedObjects.get(i).key(); + String json = readS3Object(s3, BUCKET_NAME, key); + + try { + JsonNode root = MAPPER.readTree(json); + JsonNode summary = root.path("results").path("summary"); + + int tests = summary.path("tests").asInt(); + int passed = summary.path("passed").asInt(); + double passRate = (tests == 0) ? 0.0 : (double) passed / tests * 100; + + Map entry = new LinkedHashMap<>(); + entry.put("date", extractDateFromKey(key, pattern).format(FILENAME_DATE_FORMATTER)); + entry.put("tests", tests); + entry.put("passed", passed); + entry.put("passRate", Math.round(passRate * 100.0) / 100.0); // round to 2 decimals + + historyList.add(entry); + } catch (Exception e) { + throw new RuntimeException("Failed to parse JSON from: " + key, e); + } + } + + Map result = new LinkedHashMap<>(); + result.put("language", language); + result.put("history", historyList); + + try { + return MAPPER.writeValueAsString(result); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize final result", e); + } + } + + private List listMatchingObjects(S3Client s3, String bucket, String prefix, Pattern pattern) { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucket) + .prefix(prefix) + .build(); + + ListObjectsV2Response response = s3.listObjectsV2(request); + List matched = new ArrayList<>(); + + for (S3Object obj : response.contents()) { + if (pattern.matcher(obj.key()).matches()) { + matched.add(obj); + } + } + return matched; + } + + private String readS3Object(S3Client s3, String bucket, String key) { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucket) + .key(key) + .build(); + + ResponseBytes bytes = s3.getObjectAsBytes(getObjectRequest); + return bytes.asString(StandardCharsets.UTF_8); + } + + private LocalDate extractDateFromKey(String key, Pattern pattern) { + Matcher matcher = pattern.matcher(key); + if (matcher.find()) { + return LocalDate.parse(matcher.group(1)); + } else { + throw new RuntimeException("Invalid key format: " + key); + } + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/QueryLatestFromS3.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/QueryLatestFromS3.java new file mode 100644 index 00000000000..207f33f4028 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/QueryLatestFromS3.java @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.List; +import java.util.regex.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/* + This code example is test code to query latest JSON files + used for weathertop 2 + */ +public class QueryLatestFromS3 { + + private static final Pattern FILE_PATTERN = Pattern.compile("java-(\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2})\\.json"); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm"); + + /** + * Reads a JSON file from an S3 bucket, injects a "runid" field at the top level of the JSON, + * and returns the modified JSON as a pretty-printed string. + * + *

This method performs the following steps: + *

If any exception occurs during the process (e.g., while parsing the JSON or injecting the "runid"), + * a RuntimeException is thrown with an appropriate message. + * + * @param language the language code used to determine the latest file key in the S3 bucket + * @return a pretty-printed JSON string with the "runid" field injected at the top level + * @throws RuntimeException if an error occurs while reading from S3, parsing the JSON, or injecting the "runid" + */ + public String readJsonFromS3(String language) { + String bucketName = "weathertop2"; + Region region = Region.US_EAST_1; + S3Client s3 = S3Client.builder() + .region(region) + .build(); + + String latestFileKey = getLatestFileKey(s3, bucketName, language); + System.out.println("📄 Latest file: " + latestFileKey); + + // Extract just the filename without .json + String runId = latestFileKey.replace(".json", ""); + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(latestFileKey) + .build(); + + ResponseBytes objectBytes = s3.getObjectAsBytes(getRequest); + String originalJson = objectBytes.asString(StandardCharsets.UTF_8); + + // Inject runid at top-level (not inside results) + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(originalJson); + + if (rootNode.isObject()) { + ((ObjectNode) rootNode).put("runid", runId); + } + + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode); + } catch (Exception e) { + throw new RuntimeException("Failed to parse or inject runid into JSON", e); + } + } + + /** + * Retrieves the key of the latest file in the specified S3 bucket that matches the given language prefix. + * + *

This method constructs a regex pattern to match filenames that follow the format: + * "-YYYY-MM-DDTHH-MM.json". It then lists all objects in the specified S3 bucket + * with the given prefix, filters them to match the regex pattern, sorts them by the timestamp + * extracted from the filenames in descending order, and returns the key of the first (latest) file. + * + * @param s3Client the S3Client used to interact with Amazon S3 + * @param bucketName the name of the S3 bucket to search in + * @param languagePrefix the prefix of the filenames to match (e.g., "kotlin") + * @return the key of the latest file that matches the pattern + * @throws RuntimeException if no matching files are found for the given prefix + */ + public static String getLatestFileKey(S3Client s3Client, String bucketName, String languagePrefix) { + Pattern filePattern = Pattern.compile(languagePrefix + "-(\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2})\\.json"); + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucketName) + .prefix(languagePrefix + "-") + .build(); + + ListObjectsV2Response response = s3Client.listObjectsV2(request); + List objects = response.contents(); + + return objects.stream() + .map(S3Object::key) + .filter(key -> filePattern.matcher(key).matches()) + .sorted(Comparator.comparing((String key) -> extractTimestamp(key, filePattern)).reversed()) + .findFirst() + .orElseThrow(() -> new RuntimeException("No matching files found for prefix: " + languagePrefix)); + } + + private static LocalDateTime extractTimestamp(String key, Pattern pattern) { + Matcher matcher = pattern.matcher(key); + if (matcher.find()) { + return LocalDateTime.parse(matcher.group(1), FORMATTER); + } else { + throw new RuntimeException("Invalid file format: " + key); + } + } +} diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/SDKStats.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/SDKStats.java new file mode 100644 index 00000000000..28a59283463 --- /dev/null +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/SDKStats.java @@ -0,0 +1,226 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.weathertop.service; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.regex.*; + +public class SDKStats { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm"); + + /** + * Retrieves the latest test result summaries for a set of SDK languages from an S3 bucket. + *

+ * For each language in the provided array: + *

    + *
  • Finds the most recent JSON test result file in the S3 bucket based on a timestamped filename pattern (e.g., {@code java-2025-07-10T12-15.json}).
  • + *
  • Parses the file to extract total tests and passed tests.
  • + *
  • Calculates the pass rate as {@code (passed / tests) * 100} and rounds to two decimal places.
  • + *
  • Builds a JSON response summarizing the test statistics for each language.
  • + *
+ * + * The final returned JSON has the structure: + *
+     * {
+     *   "summary": [
+     *     {
+     *       "language": "java",
+     *       "tests": 429,
+     *       "passRate": 98.37
+     *     },
+     *     {
+     *       "language": "kotlin",
+     *       "tests": 287,
+     *       "passRate": 97.56
+     *     }
+     *     ...
+     *   ]
+     * }
+     * 
+ * + * @param languages An array of SDK language prefixes (e.g., {@code ["java", "kotlin", "php"]}) used to locate result files in S3. + * @return A JSON string containing test result summaries for each language, including total tests and calculated pass rate. + * @throws JsonProcessingException If parsing or generating JSON fails. + */ + public String getCoverageSummary(String[] languages) throws JsonProcessingException { + Region region = Region.US_EAST_1; + S3Client s3Client = S3Client.builder().region(region).build(); + String bucketName = "weathertop2"; + ObjectMapper mapper = new ObjectMapper(); + + List> summaryList = new ArrayList<>(); + + for (String lang : languages) { + String prefix = lang + "-"; + Pattern timestampPattern = Pattern.compile(lang + "-(\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2})\\.json"); + + String latestKey = getLatestJsonKey(s3Client, bucketName, prefix, timestampPattern); + if (latestKey == null) { + System.out.println(" No file found for language: " + lang); + continue; + } + + // Download and parse. + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(latestKey) + .build(); + + ResponseBytes objectBytes = s3Client.getObjectAsBytes(getObjectRequest); + String jsonContent = objectBytes.asString(StandardCharsets.UTF_8); + + JsonNode root = mapper.readTree(jsonContent); + JsonNode summary = root.path("results").path("summary"); + + int tests = summary.path("tests").asInt(); + int passed = summary.path("passed").asInt(); + int failed = summary.path("failed").asInt(); + + if (tests == 0) continue; // Avoid divide by zero + + double passRate = ((double) passed / tests) * 100; + + Map langStats = new HashMap<>(); + langStats.put("language", lang); + langStats.put("tests", tests); + langStats.put("passed", passed); // ✅ use actual passed count + langStats.put("failed", failed); // ✅ add failed count + langStats.put("passRate", Math.round(passRate * 100.0) / 100.0); // round to 2 decimals + summaryList.add(langStats); + } + + Map finalResult = new HashMap<>(); + finalResult.put("summary", summaryList); + return mapper.writeValueAsString(finalResult); + } + + /** + * Retrieves the key of the most recent timestamped JSON file in the specified S3 bucket. + *

+ * This method lists objects in the given S3 bucket that match the specified prefix and timestamp pattern + * (e.g., {@code java-2025-07-10T12-15.json}). It filters the list using the provided regex {@link Pattern}, + * extracts the timestamp from each matching key, and returns the one with the latest (most recent) timestamp. + * + * @param s3Client The {@link S3Client} used to interact with Amazon S3. + * @param bucket The name of the S3 bucket containing the result files. + * @param prefix The prefix used to filter the keys (e.g., {@code "java-"}). + * @param pattern The regular expression {@link Pattern} to match and extract the timestamp from object keys. + * @return The key of the most recent matching JSON file, or {@code null} if no valid file is found. + */ + private String getLatestJsonKey(S3Client s3Client, String bucket, String prefix, Pattern pattern) { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucket) + .prefix(prefix) + .build(); + + ListObjectsV2Response response = s3Client.listObjectsV2(request); + return response.contents().stream() + .map(S3Object::key) + .filter(key -> pattern.matcher(key).matches()) + .max(Comparator.comparing(key -> extractTimestamp(key, pattern))) + .orElse(null); + } + + /** + * Extracts a {@link LocalDateTime} timestamp from an S3 object key using the provided regex pattern. + *

+ * The key must match the format defined by the pattern, typically something like: + * {@code language-yyyy-MM-ddTHH-mm.json} (e.g., {@code java-2025-07-10T12-15.json}). + * The method captures the timestamp portion, parses it using the predefined formatter, and returns it + * as a {@code LocalDateTime} object. + * + * @param key The S3 object key string (e.g., {@code java-2025-07-10T12-15.json}). + * @param pattern A {@link Pattern} containing a capturing group that extracts the timestamp portion of the key. + * @return The extracted {@link LocalDateTime} from the key. + * @throws RuntimeException if the key does not match the expected pattern. + */ + private LocalDateTime extractTimestamp(String key, Pattern pattern) { + Matcher matcher = pattern.matcher(key); + if (matcher.matches()) { + return LocalDateTime.parse(matcher.group(1), FORMATTER); + } + throw new RuntimeException("Invalid timestamp format in key: " + key); + } + + /** + * Retrieves a list of services that have no tests for each specified SDK language. + *

+ * This method scans the S3 bucket for the latest JSON test result file for each language + * (based on timestamped filenames like {@code java-2025-07-10T12-15.json}). It reads + * the {@code no_tests} section of each JSON and returns a JSON string that maps each + * SDK language to the list of services with no tests. + *

+ * + *

Example output:

+ *
+     * {
+     *   "dotnet": ["bedrock-runtime", "cognito"],
+     *   "java": ["s3", "lambda"],
+     *   "javascript": []
+     * }
+     * 
+ * + *

If no JSON file is found for a given language, the method will return an empty array + * for that language.

+ * + * @param languages An array of SDK language prefixes (e.g., {@code ["java", "dotnet", "javascript"]}) + * used to locate the result files in S3. + * @return A JSON string mapping each SDK language to an array of service names that have no tests. + * @throws JsonProcessingException If there is an error parsing or generating JSON. + */ + public String getNoTestsBySDK(String[] languages) throws JsonProcessingException { + Region region = Region.US_EAST_1; + S3Client s3Client = S3Client.builder().region(region).build(); + String bucketName = "weathertop2"; + ObjectMapper mapper = new ObjectMapper(); + + Map> sdkNoTestsMap = new HashMap<>(); + + for (String lang : languages) { + String prefix = lang + "-"; + Pattern timestampPattern = Pattern.compile(lang + "-(\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2})\\.json"); + + String latestKey = getLatestJsonKey(s3Client, bucketName, prefix, timestampPattern); + if (latestKey == null) { + System.out.println("No file found for language: " + lang); + sdkNoTestsMap.put(lang, Collections.emptyList()); + continue; + } + + // Download and parse + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(latestKey) + .build(); + + ResponseBytes objectBytes = s3Client.getObjectAsBytes(getObjectRequest); + String jsonContent = objectBytes.asString(StandardCharsets.UTF_8); + + JsonNode root = mapper.readTree(jsonContent); + JsonNode noTestsNode = root.path("results").path("no_tests"); + + List noTestsList = new ArrayList<>(); + if (noTestsNode.isArray()) { + for (JsonNode node : noTestsNode) { + noTestsList.add(node.asText()); + } + } + + sdkNoTestsMap.put(lang, noTestsList); + } + + return mapper.writeValueAsString(sdkNoTestsMap); + } +} diff --git a/.tools/weathertop/backend/src/test/java/WeatheetopTest.java b/.tools/weathertop/backend/src/test/java/WeatheetopTest.java new file mode 100644 index 00000000000..4d442ce76a7 --- /dev/null +++ b/.tools/weathertop/backend/src/test/java/WeatheetopTest.java @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import com.weathertop.service.GetLogMessages; +import com.weathertop.service.HistoricalSDK; +import com.weathertop.service.QueryLatestFromS3; +import com.weathertop.service.SDKStats; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class WeatheetopTest { + + @Test + @Order(1) + public void testStats() throws com.fasterxml.jackson.core.JsonProcessingException { + SDKStats langStats = new SDKStats(); + String[] langs = {"java", "kotlin", "dotnetv4", "php", "javascriptv3", "python", "gov2", "rustv1"}; + String JSON = assertDoesNotThrow(() -> langStats.getCoverageSummary(langs)); + System.out.println(JSON); + } + + @Test + @Order(2) + public void testGetLogs() throws com.fasterxml.jackson.core.JsonProcessingException { + GetLogMessages logs = new GetLogMessages(); + String lang = "python"; + + // Ensure the method does not throw an exception + String json = assertDoesNotThrow(() -> logs.getHistoricalSummary(lang)); + + // Print the result + System.out.println(json); + } + + + @Test + @Order(3) + public void testS3Query() throws com.fasterxml.jackson.core.JsonProcessingException { + QueryLatestFromS3 wt = new QueryLatestFromS3(); + String json = assertDoesNotThrow(() -> wt.readJsonFromS3("python")); + System.out.println(json); + } + + @Test + @Order(4) + public void testGetHistoryData() throws com.fasterxml.jackson.core.JsonProcessingException { + HistoricalSDK history = new HistoricalSDK(); + String json = assertDoesNotThrow(() -> history.getHistoricalSummary("java")); + System.out.println(json); + } +} From 0fee2a7156d13d73c52db5c460eb0eb6b6b00028 Mon Sep 17 00:00:00 2001 From: Macdonald Date: Mon, 29 Sep 2025 14:23:52 -0400 Subject: [PATCH 2/3] fixed a linter issue --- .../main/java/com/weathertop/lambda/HandlerHistoricalSDK.java | 2 ++ .../main/java/com/weathertop/service/EventBridgeScheduler.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java index 6ae0aa281b1..591ce16115a 100644 --- a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java @@ -1,3 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.weathertop.lambda; import com.amazonaws.services.lambda.runtime.Context; diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java index af0611a863e..a788fb44dfd 100644 --- a/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/service/EventBridgeScheduler.java @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package com.weathertop.service; import software.amazon.awssdk.regions.Region; From 0259983fcb3a8eb1638852c91cbdfbbcdee49061 Mon Sep 17 00:00:00 2001 From: Macdonald Date: Mon, 29 Sep 2025 14:25:24 -0400 Subject: [PATCH 3/3] fixed a linter issue --- .../main/java/com/weathertop/lambda/HandlerHistoricalSDK.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java index 591ce16115a..73cd80a516e 100644 --- a/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java +++ b/.tools/weathertop/backend/src/main/java/com/weathertop/lambda/HandlerHistoricalSDK.java @@ -8,8 +8,6 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.weathertop.service.HistoricalSDK; - - import java.util.Map; public class HandlerHistoricalSDK implements RequestHandler {