diff --git a/.github/workflows/awsapm.yml b/.github/workflows/awsapm.yml index ef7b38ae..66671fd6 100644 --- a/.github/workflows/awsapm.yml +++ b/.github/workflows/awsapm.yml @@ -1,81 +1,38 @@ -# Example workflow for Claude Code with Amazon Bedrock -# -# This workflow demonstrates how to use the Application Observability for AWS action -# with Claude Code and a custom Bedrock model. -# -# Key Features: -# - Brings your own Bedrock model (pay-per-token usage on your AWS account) -# - Uses Anthropic's official claude-code-base-action for execution -# - Two-step process: preparation + execution -# -# Prerequisites: -# 1. AWS IAM role with OIDC trust for GitHub Actions -# 2. Bedrock InvokeModel permissions in your IAM role -# 3. Application Signals and CloudWatch permissions -# -# Setup: -# 1. Create repository secret AWSAPM_ROLE_ARN with your IAM role ARN -# 2. (Optional) Set repository variable AWS_REGION for your preferred region -# 3. Copy this file to .github/workflows/awsapm.yml in your repository - -name: Application observability for AWS (Claude + Bedrock) +name: Application observability for AWS on: issue_comment: types: [created, edited] + pull_request_review_comment: + types: [created] issues: types: [opened, assigned, edited] + pull_request_review: + types: [submitted] jobs: awsapm-investigation: - # Only run when @awsapm is mentioned if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@awsapm')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@awsapm')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@awsapm')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@awsapm') || contains(github.event.issue.title, '@awsapm'))) runs-on: ubuntu-latest - permissions: - contents: write # To create branches for PRs - pull-requests: write # To post comments on PRs - issues: write # To post comments on issues - id-token: write # Required for AWS OIDC authentication - + contents: write + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ vars.AWS_REGION || 'us-east-1' }} - - # Step 1: Prepare AWS MCP configuration and investigation prompt - - name: Prepare Investigation Context - id: prepare - uses: aws-actions/application-observability-for-aws@v1 - with: - bot_name: "@awsapm" - cli_tool: "claude_code" - - # Step 2: Execute investigation with Claude Code - - name: Run Claude Investigation - id: claude - uses: anthropics/claude-code-base-action@beta with: - use_bedrock: "true" - model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0" - prompt_file: ${{ steps.prepare.outputs.prompt_file }} - mcp_config: ${{ steps.prepare.outputs.mcp_config_file }} - allowed_tools: ${{ steps.prepare.outputs.allowed_tools }} + fetch-depth: 1 - # Step 3: Post results back to GitHub issue/PR (reuse the same action) - - name: Post Investigation Results - if: always() # Run even if Claude step fails - uses: aws-actions/application-observability-for-aws@v1 + - name: Run Application observability for AWS Investigation + id: awsapm + uses: mxiamxia/aws-apm-action@main with: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_region: ${{ vars.AWS_REGION || 'us-east-1' }} bot_name: "@awsapm" - cli_tool: "claude_code" - comment_id: ${{ steps.prepare.outputs.awsapm_comment_id }} - output_file: ${{ steps.claude.outputs.execution_file }} - output_status: ${{ steps.claude.outputs.conclusion }} diff --git a/ai-validator/libs/tests/test-billing-summary.script.md b/ai-validator/libs/tests/test-billing-summary.script.md deleted file mode 100644 index 4453cf30..00000000 --- a/ai-validator/libs/tests/test-billing-summary.script.md +++ /dev/null @@ -1,61 +0,0 @@ -## Steps - -### 1. Navigate to CloudWatch Application Signals Services - -Go to CloudWatch → Application Signals (APM) → Services. - -### 2. Search for billing-service-python - -In the search field with placeholder text 'Filter services and resources by text, property or value', type 'billing-service-python' and press Enter. - -**Constraints:** -- You MUST ensure you press Enter. - -### 3. Click on billing-service-python - -Click the hyperlink 'billing-service-python'. - -### 4. Click 'Service operation' tab - -Click the 'Service operation' tab. Then, ensure the `GET ^summary/$` is selected if not already. - -### 5. Click on a datapoint for GET /summary - -Click on a datapoint in the GET /summary operation graph, PASS in 1 and 3 as a PARAMETERS. - -**Constraints:** -- You MUST pass in parameters 1 and 3 - -### 6. Click 'Correlate with Other Metrics' - -In the diagnostic drawer on the right, click the 'Correlate with Other Metrics' button. - -### 7. Check you are on Related metrics tab - -Check that you are now on the 'Related metrics' tab. - -### 8. Search for BillingSummaryCacheHitCount metric - -In the search field with placeholder text 'Filter metrics by text, property, or value', type 'BillingSummaryCacheHitCount' and press Enter. - -**Constraints:** -- You MUST ensure you press Enter. - -### 9. Select BillingSummaryCacheHitCount metric - -In the metrics table, select the 'BillingSummaryCacheHitCount' metric to add it to the graph. - -### 10. Search for BillingSummaryCacheMissCount metric - -In the search field with placeholder text 'Filter metrics by text, property, or value', type 'BillingSummaryCacheMissCount' and press Enter. - -**Constraints:** -- You MUST ensure you press Enter. - -### 11. Select BillingSummaryCacheMissCount metric - -In the metrics table, select the 'BillingSummaryCacheMissCount' metric to add it to the graph. - -### 12. Check correlation between metrics - -Check that when latency spikes occur, BillingSummaryCacheHitCount decreases and BillingSummaryCacheMissCount increases at the same time periods. \ No newline at end of file diff --git a/data_test/test_cases/metrics_test_cases.json b/data_test/test_cases/metrics_test_cases.json index bf9a44cc..9ba365d8 100644 --- a/data_test/test_cases/metrics_test_cases.json +++ b/data_test/test_cases/metrics_test_cases.json @@ -60,27 +60,6 @@ }] }, "notes": "Corresponds to PDF scenario 2" - }, - { - "test_case_id": "billing_summary_service_metric_availability_check", - "description": "Verify Service Latency metric availability for billing summary API (Scenario 16)", - "test_scenario": "Scenario 16", - "metric_namespace": "ApplicationSignals", - "metric_name": "Latency", - "statistic": "p99", - "dimensions": [ - {"Name": "Operation", "Value": "GET ^summary/$"}, - {"Name": "Environment", "Value": "ENVIRONMENT_NAME_PLACEHOLDER"}, - {"Name": "Service", "Value": "billing-service-python"} - ], - "evaluation_period_minutes": 180, - "threshold": { - "comparison_operator": [{ - "operator": "GreaterThanOrEqualToThreshold", - "threshold_value": 0 - }] - }, - "notes": "Corresponds to PDF scenario 16" }, { "test_case_id": "database_insight_dependency_metric_availability_check", diff --git a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py index 48a4b757..83878743 100644 --- a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py +++ b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py @@ -76,10 +76,6 @@ def create_nutrition_agent(): system_prompt = ( "You are a specialized pet nutrition expert at our veterinary clinic, providing accurate, evidence-based dietary guidance for pets. " "Never mention using any API, tools, or external services - present all advice as your own expert knowledge.\n\n" - "CRITICAL VALIDATION RULES:\n" - "1. ALWAYS check if the 'products' field is empty or contains an error message before making product recommendations\n" - "2. If the products field is empty or contains an error (starts with 'Error:'), NEVER recommend products from your training data\n" - "3. Instead, respond: 'We currently don't have nutrition products available for [pet type]. Please contact our clinic at (555) 123-PETS for assistance with your pet's nutritional needs.'\n\n" "When providing nutrition guidance:\n" "- Use the specific nutrition information available to you as the foundation for your recommendations\n" "- Always recommend the SPECIFIC PRODUCT NAMES provided to you that pet owners should buy FROM OUR PET CLINIC\n" @@ -110,4 +106,4 @@ async def invoke(payload, context): return ''.join(response_data) if __name__ == "__main__": - uvicorn.run(agent_app, host='0.0.0.0', port=8080) + uvicorn.run(agent_app, host='0.0.0.0', port=8080) \ No newline at end of file diff --git a/pet_clinic_billing_service/billing_service/views.py b/pet_clinic_billing_service/billing_service/views.py index 214e6814..549a99f1 100644 --- a/pet_clinic_billing_service/billing_service/views.py +++ b/pet_clinic_billing_service/billing_service/views.py @@ -1,8 +1,6 @@ from rest_framework import viewsets, status from rest_framework.response import Response -from django.db.models import Subquery, Count, Sum -from django.utils import timezone -from django.core.cache import cache +from django.db.models import Subquery from .models import Billing,CheckList from .serializers import BillingSerializer from opentelemetry import trace @@ -156,44 +154,6 @@ def log(self, data): # Don't raise the exception to avoid disrupting the main flow -class SummaryViewSet(viewsets.ViewSet): - def list(self, request, pk=None): - span = trace.get_current_span() - - # Always set request counter to 1 - span.set_attribute("billing_summary_request", 1) - - # Set num_summaries based on current minute - current_minute = timezone.now().minute - num_summaries = 50 if current_minute % 5 == 0 else 2 - - # Use random cache key to simulate high cache miss rate - cache_key = f'billing_summary_last_7_days_{random.randint(1, num_summaries)}' - summary = cache.get(cache_key) - - if summary is None: - # Cache miss - span.set_attribute("billing_summary_cache_hit", 0) - - # Sleep to simulate high latency when there's a cache miss - time.sleep(2) - - billings = Billing.objects.all() - - summary = { - 'total_count': billings.count(), - 'total_amount': billings.aggregate(Sum('payment'))['payment__sum'] or 0, - 'period': 'all_time' - } - - cache.set(cache_key, summary, 300) # Cache for 5 minutes - else: - # Cache hit - span.set_attribute("billing_summary_cache_hit", 1) - - return Response(summary) - - class HealthViewSet(viewsets.ViewSet): def list(self, request): logger.info("HealthViewSet.list() called - Health check requested") diff --git a/pet_clinic_billing_service/create-metric-filters.sh b/pet_clinic_billing_service/create-metric-filters.sh deleted file mode 100644 index 927517e5..00000000 --- a/pet_clinic_billing_service/create-metric-filters.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash - -# Set variables -LOG_GROUP_NAME="aws/spans" -REGION=${AWS_REGION:-us-east-1} -NAMESPACE="CustomBilling" - -# TODO: [@lnnofal] convert it to CDK and hooked up with other CDK stacks (for EKS) -echo "Creating log group if it doesn't exist..." -aws logs create-log-group --log-group-name "$LOG_GROUP_NAME" --region "$REGION" 2>/dev/null || echo "Log group already exists or creation failed" - -echo "Creating CloudWatch metric filters for billing service custom metrics..." - -# Create metric filter for billing summary requests -cat > metric-filter.json << EOF -{ -"logGroupName": "aws/spans", -"filterName": "BillingSummaryRequests", -"filterPattern": "{ $.attributes.['billing_summary_request'] = \"1\" }", -"metricTransformations": [ -{ -"metricName": "BillingSummaryRequestCount", -"metricNamespace": "$NAMESPACE", -"metricValue": "1", -"unit": "Count", -"dimensions": { -"Service": "$.attributes.['aws.local.service']", -"Environment": "$.attributes.['aws.local.environment']", -"Operation": "$.attributes.['aws.local.operation']" -} -} -] -} -EOF - -aws logs put-metric-filter --region $REGION --cli-input-json file://metric-filter.json || { rm -f metric-filter.json; exit 1; } - -# Create metric filter for cache hits -cat > metric-filter.json << EOF -{ -"logGroupName": "aws/spans", -"filterName": "BillingSummaryCacheHits", -"filterPattern": "{ $.attributes.['billing_summary_cache_hit'] = \"1\" }", -"metricTransformations": [ -{ -"metricName": "BillingSummaryCacheHitCount", -"metricNamespace": "$NAMESPACE", -"metricValue": "1", -"unit": "Count", -"dimensions": { -"Service": "$.attributes.['aws.local.service']", -"Environment": "$.attributes.['aws.local.environment']", -"Operation": "$.attributes.['aws.local.operation']" -} -} -] -} -EOF - -aws logs put-metric-filter --region $REGION --cli-input-json file://metric-filter.json || { rm -f metric-filter.json; exit 1; } - -# Create metric filter for cache misses -cat > metric-filter.json << EOF -{ -"logGroupName": "aws/spans", -"filterName": "BillingSummaryCacheMisses", -"filterPattern": "{ $.attributes.['billing_summary_cache_hit'] = \"0\" }", -"metricTransformations": [ -{ -"metricName": "BillingSummaryCacheMissCount", -"metricNamespace": "$NAMESPACE", -"metricValue": "1", -"unit": "Count", -"dimensions": { -"Service": "$.attributes.['aws.local.service']", -"Environment": "$.attributes.['aws.local.environment']", -"Operation": "$.attributes.['aws.local.operation']" -} -} -] -} -EOF - -aws logs put-metric-filter --region $REGION --cli-input-json file://metric-filter.json || { rm -f metric-filter.json; exit 1; } - -rm -f metric-filter.json - -echo "Metric filters created successfully!" -echo "Metrics will appear in CloudWatch under the '$NAMESPACE' namespace:" -echo "- BillingSummaryRequestCount" -echo "- BillingSummaryCacheHitCount" -echo "- BillingSummaryCacheMissCount" \ No newline at end of file diff --git a/pet_clinic_billing_service/pet_clinic_billing_service/urls.py b/pet_clinic_billing_service/pet_clinic_billing_service/urls.py index 346ce2f0..517c153c 100644 --- a/pet_clinic_billing_service/pet_clinic_billing_service/urls.py +++ b/pet_clinic_billing_service/pet_clinic_billing_service/urls.py @@ -17,11 +17,10 @@ from django.contrib import admin from django.urls import include, path from rest_framework.routers import DefaultRouter -from billing_service.views import HealthViewSet, BillingViewSet, SummaryViewSet +from billing_service.views import HealthViewSet, BillingViewSet router = DefaultRouter() router.register('billings', BillingViewSet, basename='billings') -router.register('summary', SummaryViewSet, basename='summary') router.register('health', HealthViewSet, basename='health') urlpatterns = [ path("admin/", admin.site.urls), diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/BillingServiceClient.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/BillingServiceClient.java index 252a98d7..e56baa7f 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/BillingServiceClient.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/BillingServiceClient.java @@ -45,11 +45,4 @@ public Flux getBillings() { .bodyToFlux(BillingDetail.class); } - public Mono getBillingSummary() { - return webClientBuilder.build().get() - .uri("http://billing-service/summary/") - .retrieve() - .bodyToMono(Object.class); - } - } diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiController.java index 2d38cc67..d40a638f 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiController.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiController.java @@ -122,11 +122,6 @@ public Flux getBillings() { return billingServiceClient.getBillings(); } - @GetMapping(value = "billing/summary") - public Mono getBillingSummary() { - return billingServiceClient.getBillingSummary(); - } - @PostMapping(value = "insurance/pet-insurances") public Mono addPetInsurance(final @RequestBody PetInsurance petInsurance) { System.out.println(petInsurance.toString()); diff --git a/traffic-generator/index.js b/traffic-generator/index.js index 6fbb6f3b..eb9c0948 100644 --- a/traffic-generator/index.js +++ b/traffic-generator/index.js @@ -251,13 +251,3 @@ const lowTrafficBillingTask = cron.schedule('*/2 * * * *', () => { }, { scheduled: false }); lowTrafficBillingTask.start(); - -const lowTrafficBillingSummaryTask = cron.schedule('* * * * * *', () => { - console.log('query billing summary every 1 second'); - axios.get(`${baseUrl}/api/billing/summary`, { timeout: 10000 }) - .catch(err => { - console.error(`${baseUrl}/api/billing/summary, error: ` + (err.response ? err.response.data : err.toString())); - }); // Catch and log errors -}, { scheduled: false }); - -lowTrafficBillingSummaryTask.start();