diff --git a/.pipelines/azure_pipeline_validation_appmonitoring.yaml b/.pipelines/azure_pipeline_validation_appmonitoring.yaml index a09de9220..f3a3b6b0e 100644 --- a/.pipelines/azure_pipeline_validation_appmonitoring.yaml +++ b/.pipelines/azure_pipeline_validation_appmonitoring.yaml @@ -29,8 +29,12 @@ variables: pythonTestAppName: 'python-test-app' dotnetTestAppImageName: '${{ variables.containerRegistry }}.azurecr.io/demoaks-dotnet-app:latest' dotnetTestAppName: 'dotnet-test-app' + goTestAppImageName: '${{ variables.containerRegistry }}.azurecr.io/demoaks-go-instrumented-app:latest' + goTestAppName: 'go-instrumented-test-app' testNamespace: 'test-ns' - aiResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourceGroups/aks-appmonitoring-pipeline/providers/microsoft.insights/components/appmonitoring-pipeline-validation-ai' + aiResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourcegroups/aks-appmonitoring-pipeline/providers/microsoft.insights/components/appmonitoring-pipeline-validation-ai-otel' + lawResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourcegroups/ai_appmonitoring-pipeline-validation-ai-ote_37743a46-5226-447c-842b-35fac54dbd92_managed/providers/microsoft.operationalinsights/workspaces/managed-appmonitoring-pipeline-validation-ai-otel-ws' + amwQueryEndpoint: 'https://managed-appmonitoring-pipeline-validatio-amw-axfudjacdrgbe5ht.eastus.prometheus.monitor.azure.com' Codeql.Enabled: true Codeql.BuildIdentifier: 'linuxbuild' AKSResourceGroup: 'aks-appmonitoring-pipeline' @@ -296,11 +300,13 @@ jobs: export NODEJS_TEST_IMAGE_NAME=${{ variables.nodeTestAppImageName }} export PYTHON_TEST_IMAGE_NAME=${{ variables.pythonTestAppImageName }} export DOTNET_TEST_IMAGE_NAME=${{ variables.dotnetTestAppImageName }} + export GO_TEST_IMAGE_NAME=${{ variables.goTestAppImageName }} export JAVA_TEST_APP_NAME="${{ variables.javaTestAppName }}" export NODEJS_TEST_APP_NAME="${{ variables.nodeTestAppName }}" export PYTHON_TEST_APP_NAME="${{ variables.pythonTestAppName }}" export DOTNET_TEST_APP_NAME="${{ variables.dotnetTestAppName }}" + export GO_TEST_APP_NAME="${{ variables.goTestAppName }}" export TEST_APP_SOURCE_NAME="nodejs-source-app" export NODEJS_CALLER_APP_NAME="nodejs-caller-app" @@ -328,13 +334,13 @@ jobs: sudo chmod u+x ./validate-mutation.sh - if ! ./validate-mutation.sh ${{ variables.javaTestAppName }} ${{ variables.nodeTestAppName }} ${{ variables.pythonTestAppName }} ${{ variables.dotnetTestAppName }} ${{ variables.testNamespace }}; then + if ! ./validate-mutation.sh ${{ variables.javaTestAppName }} ${{ variables.nodeTestAppName }} ${{ variables.pythonTestAppName }} ${{ variables.dotnetTestAppName }} ${{ variables.goTestAppName }} ${{ variables.testNamespace }}; then echo "Mutation validation failed" exit 1 fi - task: AzureCLI@2 - displayName: "Check test apps are sending telemetry to AI" + displayName: "Check test apps are sending AI and OTEL telemetry" inputs: azureSubscription: ${{ variables.armServiceConnectionName }} scriptType: bash @@ -348,17 +354,31 @@ jobs: export NODEJS_TEST_APP_NAME="${{ variables.nodeTestAppName }}" export PYTHON_TEST_APP_NAME="${{ variables.pythonTestAppName }}" export DOTNET_TEST_APP_NAME="${{ variables.dotnetTestAppName }}" + export GO_TEST_APP_NAME="${{ variables.goTestAppName }}" - echo "Wait 30s for telemetry to flow..." - sleep 30 - sudo chmod u+x ./validate_ai.sh - if ! ./validate_ai.sh ${{ variables.aiResourceId }} ${{ variables.testNamespace }}; then + echo "Validating AI telemetry..." + if ! ./validate_ai.sh ${{ variables.lawResourceId }} ${{ variables.testNamespace }} "java,nodejs,python,dotnet" "AppRoleInstance" "AppRequests" "AppDependencies" "AppMetrics" "AppExceptions"; then echo "AI telemetry validation failed" exit 1 fi + echo "Validating OTEL telemetry..." + if ! ./validate_ai.sh ${{ variables.lawResourceId }} ${{ variables.testNamespace }} "go" "ServiceInstanceId" "OTelSpans" "OTelResources" "OTelLogs"; then + echo "OTEL telemetry validation failed" + exit 1 + fi + + echo "Validating AMW metrics..." + sudo chmod u+x ./validate_amw.sh + # TEMPORARY: Exclude dotnet from AMW validation due to bug where DotNet metrics are not emitted to AMW + # TODO: Revert to "java,nodejs,python,dotnet,go" once DotNet AMW metrics bug is fixed + if ! ./validate_amw.sh ${{ variables.amwQueryEndpoint }} ${{ variables.testNamespace }} "java,nodejs,python,go"; then + echo "AMW metrics validation failed" + exit 1 + fi + - task: AzureCLI@2 displayName: "Validate Housekeeper Cron Job" inputs: diff --git a/.pipelines/azure_pipeline_validation_appmonitoring_extension.yaml b/.pipelines/azure_pipeline_validation_appmonitoring_extension.yaml index 9a2c39817..1f1646c3b 100644 --- a/.pipelines/azure_pipeline_validation_appmonitoring_extension.yaml +++ b/.pipelines/azure_pipeline_validation_appmonitoring_extension.yaml @@ -22,9 +22,13 @@ variables: pythonTestAppName: 'python-test-app' dotnetTestAppImageName: '${{ variables.containerRegistry }}.azurecr.io/demoaks-dotnet-app:latest' dotnetTestAppName: 'dotnet-test-app' + goTestAppImageName: '${{ variables.containerRegistry }}.azurecr.io/demoaks-go-instrumented-app:latest' + goTestAppName: 'go-instrumented-test-app' testNamespace: 'test-ns' aiConnectionString: 'InstrumentationKey=2b453402-fcfb-408f-8495-c551f0e82f46;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/' - aiResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourceGroups/aks-appmonitoring-pipeline/providers/microsoft.insights/components/appmonitoring-pipeline-validation-ai' + aiResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourceGroups/aks-appmonitoring-pipeline/providers/microsoft.insights/components/appmonitoring-pipeline-validation-ai-otel' + lawResourceId: '/subscriptions/5a3b3ba4-3a42-42ae-b2cb-f882345803bc/resourcegroups/ai_appmonitoring-pipeline-validation-ai-ote_37743a46-5226-447c-842b-35fac54dbd92_managed/providers/microsoft.operationalinsights/workspaces/managed-appmonitoring-pipeline-validation-ai-otel-ws' + amwQueryEndpoint: 'https://managed-appmonitoring-pipeline-validatio-amw-axfudjacdrgbe5ht.eastus.prometheus.monitor.azure.com' Codeql.Enabled: true Codeql.BuildIdentifier: 'linuxbuild' AKSResourceGroup: 'aks-appmonitoring-pipeline' @@ -348,11 +352,13 @@ jobs: export NODEJS_TEST_IMAGE_NAME=${{ variables.nodeTestAppImageName }} export PYTHON_TEST_IMAGE_NAME=${{ variables.pythonTestAppImageName }} export DOTNET_TEST_IMAGE_NAME=${{ variables.dotnetTestAppImageName }} + export GO_TEST_IMAGE_NAME=${{ variables.goTestAppImageName }} export JAVA_TEST_APP_NAME="${{ variables.javaTestAppName }}" export NODEJS_TEST_APP_NAME="${{ variables.nodeTestAppName }}" export PYTHON_TEST_APP_NAME="${{ variables.pythonTestAppName }}" export DOTNET_TEST_APP_NAME="${{ variables.dotnetTestAppName }}" + export GO_TEST_APP_NAME="${{ variables.goTestAppName }}" export TEST_APP_SOURCE_NAME="nodejs-source-app" export NODEJS_CALLER_APP_NAME="nodejs-caller-app" @@ -377,13 +383,13 @@ jobs: sudo chmod u+x ./validate-mutation.sh - if ! ./validate-mutation.sh ${{ variables.javaTestAppName }} ${{ variables.nodeTestAppName }} ${{ variables.pythonTestAppName }} ${{ variables.dotnetTestAppName }} ${{ variables.testNamespace }}; then + if ! ./validate-mutation.sh ${{ variables.javaTestAppName }} ${{ variables.nodeTestAppName }} ${{ variables.pythonTestAppName }} ${{ variables.dotnetTestAppName }} ${{ variables.goTestAppName }} ${{ variables.testNamespace }}; then echo "Mutation validation failed" exit 1 fi - task: AzureCLI@2 - displayName: "Check test apps are sending telemetry to AI" + displayName: "Check test apps are sending AI and OTEL telemetry" inputs: azureSubscription: ${{ variables.armServiceConnectionName }} scriptType: bash @@ -397,17 +403,31 @@ jobs: export NODEJS_TEST_APP_NAME="${{ variables.nodeTestAppName }}" export PYTHON_TEST_APP_NAME="${{ variables.pythonTestAppName }}" export DOTNET_TEST_APP_NAME="${{ variables.dotnetTestAppName }}" + export GO_TEST_APP_NAME="${{ variables.goTestAppName }}" - echo "Wait 30s for telemetry to flow..." - sleep 30 - sudo chmod u+x ./validate_ai.sh - if ! ./validate_ai.sh ${{ variables.aiResourceId }} ${{ variables.testNamespace }}; then + echo "Validating AI telemetry..." + if ! ./validate_ai.sh ${{ variables.lawResourceId }} ${{ variables.testNamespace }} "java,nodejs,python,dotnet" "AppRoleInstance" "AppRequests" "AppDependencies" "AppMetrics" "AppExceptions"; then echo "AI telemetry validation failed" exit 1 fi + echo "Validating OTEL telemetry..." + if ! ./validate_ai.sh ${{ variables.lawResourceId }} ${{ variables.testNamespace }} "go" "ServiceInstanceId" "OTelSpans" "OTelResources" "OTelLogs"; then + echo "OTEL telemetry validation failed" + exit 1 + fi + + echo "Validating AMW metrics..." + sudo chmod u+x ./validate_amw.sh + # TEMPORARY: Exclude dotnet from AMW validation due to bug where DotNet metrics are not emitted to AMW + # TODO: Revert to "java,nodejs,python,dotnet,go" once DotNet AMW metrics bug is fixed + if ! ./validate_amw.sh ${{ variables.amwQueryEndpoint }} ${{ variables.testNamespace }} "java,nodejs,python,go"; then + echo "AMW metrics validation failed" + exit 1 + fi + - task: AzureCLI@2 displayName: "Validate Housekeeper Cron Job" inputs: diff --git a/appmonitoring/scripts/install-test-apps.sh b/appmonitoring/scripts/install-test-apps.sh index 496fa25ec..d5f5b6543 100644 --- a/appmonitoring/scripts/install-test-apps.sh +++ b/appmonitoring/scripts/install-test-apps.sh @@ -18,11 +18,13 @@ require_env JAVA_TEST_APP_NAME require_env NODEJS_TEST_APP_NAME require_env PYTHON_TEST_APP_NAME require_env DOTNET_TEST_APP_NAME +require_env GO_TEST_APP_NAME require_env NODEJS_CALLER_APP_NAME require_env JAVA_TEST_IMAGE_NAME require_env NODEJS_TEST_IMAGE_NAME require_env PYTHON_TEST_IMAGE_NAME require_env DOTNET_TEST_IMAGE_NAME +require_env GO_TEST_IMAGE_NAME if ! command -v envsubst >/dev/null 2>&1; then echo "Error: envsubst command not found" @@ -36,12 +38,14 @@ JAVA_RELEASE_NAME=${JAVA_TEST_APP_NAME} NODEJS_RELEASE_NAME=${NODEJS_TEST_APP_NAME} PYTHON_RELEASE_NAME=${PYTHON_TEST_APP_NAME} DOTNET_RELEASE_NAME=${DOTNET_TEST_APP_NAME} +GO_RELEASE_NAME=${GO_TEST_APP_NAME} CALLER_RELEASE_NAME=${NODEJS_CALLER_APP_NAME} JAVA_SERVICE_HOST="${JAVA_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local" NODEJS_SERVICE_HOST="${NODEJS_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local" PYTHON_SERVICE_HOST="${PYTHON_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local" DOTNET_SERVICE_HOST="${DOTNET_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local" +GO_SERVICE_HOST="${GO_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local" SOURCE_SERVICE_URL="http://${SOURCE_RELEASE_NAME}-service.${TEST_NS}.svc.cluster.local:3001" # Delete existing test apps if they exist - TEMPORARY - WILL BE REMOVED LATER @@ -50,6 +54,7 @@ cat ../validation-helm/test-apps/java/chart.yaml | envsubst | kubectl delete -f cat ../validation-helm/test-apps/nodejs/chart.yaml | envsubst | kubectl delete -f - --ignore-not-found cat ../validation-helm/test-apps/python/chart.yaml | envsubst | kubectl delete -f - --ignore-not-found cat ../validation-helm/test-apps/dotnet/chart.yaml | envsubst | kubectl delete -f - --ignore-not-found +cat ../validation-helm/test-apps/go-instrumented/chart.yaml | envsubst | kubectl delete -f - --ignore-not-found cat ../validation-helm/test-apps/testappcaller/chart.yaml | envsubst | kubectl delete -f - --ignore-not-found @@ -59,6 +64,7 @@ helm uninstall -n ${TEST_NS} "${JAVA_RELEASE_NAME}" --ignore-not-found 2>/dev/nu helm uninstall -n ${TEST_NS} "${NODEJS_RELEASE_NAME}" --ignore-not-found 2>/dev/null || true helm uninstall -n ${TEST_NS} "${PYTHON_RELEASE_NAME}" --ignore-not-found 2>/dev/null || true helm uninstall -n ${TEST_NS} "${DOTNET_RELEASE_NAME}" --ignore-not-found 2>/dev/null || true +helm uninstall -n ${TEST_NS} "${GO_RELEASE_NAME}" --ignore-not-found 2>/dev/null || true helm uninstall -n ${TEST_NS} "${CALLER_RELEASE_NAME}" --ignore-not-found 2>/dev/null || true @@ -124,6 +130,16 @@ if ! helm install "${DOTNET_RELEASE_NAME}" oci://${ACR_NAME}/helm/testapps/dotne exit 1 fi +# this is the instrumented go app +echo "Installing ${GO_RELEASE_NAME}..." +if ! helm install "${GO_RELEASE_NAME}" oci://${ACR_NAME}/helm/testapps/go-instrumented-test-app --version "${CHART_VERSION}" -n "${TEST_NS}" \ + --set-string appName="${GO_RELEASE_NAME}" \ + --set-string image="${GO_TEST_IMAGE_NAME}" \ + --set-string targetUrl="${SOURCE_SERVICE_URL}"; then + echo "Error: ${GO_RELEASE_NAME} installation failed" + exit 1 +fi + # this is the app that will periodically call the instrumented apps to generate request telemetry echo "Installing ${CALLER_RELEASE_NAME}..." if ! helm install "${CALLER_RELEASE_NAME}" oci://${ACR_NAME}/helm/testapps/testappcaller --version "${CHART_VERSION}" -n "${TEST_NS}" \ @@ -131,7 +147,8 @@ if ! helm install "${CALLER_RELEASE_NAME}" oci://${ACR_NAME}/helm/testapps/testa --set-string javaHost="${JAVA_SERVICE_HOST}" \ --set-string nodejsHost="${NODEJS_SERVICE_HOST}" \ --set-string pythonHost="${PYTHON_SERVICE_HOST}" \ - --set-string dotnetHost="${DOTNET_SERVICE_HOST}"; then + --set-string dotnetHost="${DOTNET_SERVICE_HOST}" \ + --set-string goHost="${GO_SERVICE_HOST}"; then echo "Error: ${CALLER_RELEASE_NAME} installation failed" exit 1 fi diff --git a/appmonitoring/scripts/validate-mutation.sh b/appmonitoring/scripts/validate-mutation.sh index 3b6c077fe..750ddc6d9 100755 --- a/appmonitoring/scripts/validate-mutation.sh +++ b/appmonitoring/scripts/validate-mutation.sh @@ -5,7 +5,8 @@ DEPLOYMENT_JAVA_NAME=$1 DEPLOYMENT_NODEJS_NAME=$2 DEPLOYMENT_PYTHON_NAME=$3 DEPLOYMENT_DOTNET_NAME=$4 -NAMESPACE=$5 +DEPLOYMENT_GO_NAME=$5 +NAMESPACE=$6 # Define the property to check for PROPERTY="APPLICATIONINSIGHTS_CONNECTION_STRING" @@ -14,6 +15,7 @@ JAVA_DEPLOYMENT_NAME=$(kubectl get deployment -n "$NAMESPACE" -o custom-columns= NODEJS_DEPLOYMENT_NAME=$(kubectl get deployment -n "$NAMESPACE" -o custom-columns=NAME:.metadata.name | grep "$DEPLOYMENT_NODEJS_NAME") PYTHON_DEPLOYMENT_NAME=$(kubectl get deployment -n "$NAMESPACE" -o custom-columns=NAME:.metadata.name | grep "$DEPLOYMENT_PYTHON_NAME") DOTNET_DEPLOYMENT_NAME=$(kubectl get deployment -n "$NAMESPACE" -o custom-columns=NAME:.metadata.name | grep "$DEPLOYMENT_DOTNET_NAME") +GO_DEPLOYMENT_NAME=$(kubectl get deployment -n "$NAMESPACE" -o custom-columns=NAME:.metadata.name | grep "$DEPLOYMENT_GO_NAME") EXPECTED_ENV_VARS=( "NODE_NAME" @@ -41,6 +43,8 @@ DOTNET_ENV_VARS=( "OTEL_DOTNET_AUTO_PLUGINS" "OTEL_DOTNET_AUTO_LOGS_ENABLED" ) +GO_ENV_VARS=( +) EXPECTED_INIT_CONTAINERS=( "azure-monitor-auto-instrumentation-java" @@ -52,6 +56,8 @@ PYTHON_EXPECTED_INIT_CONTAINERS=( DOTNET_EXPECTED_INIT_CONTAINERS=( "azure-monitor-auto-instrumentation-dotnet" ) +GO_EXPECTED_INIT_CONTAINERS=( +) checkMutation() { local deploymentName="$1" @@ -126,3 +132,8 @@ if ! checkMutation "$DEPLOYMENT_DOTNET_NAME" DOTNET_ENV_VARS[@] DOTNET_EXPECTED_ echo "FATAL ERROR: checkMutation failed for $DEPLOYMENT_DOTNET_NAME" exit 1 fi + +if ! checkMutation "$DEPLOYMENT_GO_NAME" GO_ENV_VARS[@] GO_EXPECTED_INIT_CONTAINERS[@]; then + echo "FATAL ERROR: checkMutation failed for $DEPLOYMENT_GO_NAME" + exit 1 +fi diff --git a/appmonitoring/scripts/validate_ai.sh b/appmonitoring/scripts/validate_ai.sh index 0f54a4ac0..f76c194ea 100644 --- a/appmonitoring/scripts/validate_ai.sh +++ b/appmonitoring/scripts/validate_ai.sh @@ -1,34 +1,50 @@ #!/bin/bash -AI_RES_ID=$1 +WS_RES_ID=$1 NAMESPACE=$2 +APPS_TO_VALIDATE=$3 # Comma-separated list of apps (e.g., "java,nodejs,python,dotnet" or "go") +ROLE_INSTANCE_FIELD=$4 +shift 4 # Remove first 4 arguments +QUERIES=("$@") # Remaining arguments are the queries + +# Validate that apps parameter is provided +if [[ -z "$APPS_TO_VALIDATE" ]]; then + echo "Error: APPS_TO_VALIDATE parameter is required (3rd argument)" >&2 + echo "Usage: $0 " >&2 + exit 1 +fi -echo "Finding pods in namespace: $NAMESPACE for Java App $JAVA_TEST_APP_NAME, NodeJS App $NODEJS_TEST_APP_NAME, Python App $PYTHON_TEST_APP_NAME, and Dotnet App $DOTNET_TEST_APP_NAME" +echo "Finding pods in namespace: $NAMESPACE for Java App $JAVA_TEST_APP_NAME, NodeJS App $NODEJS_TEST_APP_NAME, Python App $PYTHON_TEST_APP_NAME, Dotnet App $DOTNET_TEST_APP_NAME, and Go App $GO_TEST_APP_NAME" POD_JAVA_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$JAVA_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) POD_NODEJS_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$NODEJS_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) POD_PYTHON_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$PYTHON_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) POD_DOTNET_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$DOTNET_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) +POD_GO_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$GO_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) # Get an access token result_rsp=$(curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.applicationinsights.io&mi_res_id=/subscriptions/66010356-d8a5-42d3-8593-6aaa3aeb1c11/resourceGroups/rambhatt-rnd-v2/providers/Microsoft.ManagedIdentity/userAssignedIdentities/rambhatt-agentpool-es-identity' -H Metadata:true -s) # echo "Result: $result_rsp" access_token=$(echo $result_rsp | jq -r '.access_token') +client_id=$(echo $result_rsp | jq -r '.client_id') -echo "$AI_RES_ID" +echo "Using identity with client_id: $client_id" +echo "Workspace: $WS_RES_ID" +echo "Role instance field: $ROLE_INSTANCE_FIELD" # Define your variables -url="https://api.loganalytics.io/v1$AI_RES_ID/query" +url="https://api.loganalytics.io/v1$WS_RES_ID/query" verify_AI_telemetry() { local pod_name="$1" local app_type="$2" local skip_exceptions="$3" - local queries=("requests" "dependencies") + local tables=("${QUERIES[@]}") local found_any=0 - if [[ "$skip_exceptions" != "true" ]]; then - queries+=("exceptions") + # Remove AppExceptions from tables if skip_exceptions is true + if [[ "$skip_exceptions" == "true" ]]; then + tables=("${tables[@]/AppExceptions/}") fi echo "Validating telemetry for $pod_name ($app_type)..." @@ -37,9 +53,14 @@ verify_AI_telemetry() { exit 1 fi - for table in "${queries[@]}"; do + for table in "${tables[@]}"; do + # Skip empty entries (from removed AppExceptions) + [[ -z "$table" ]] && continue + + query="$table | where TimeGenerated > ago(15m) | where $ROLE_INSTANCE_FIELD == '$pod_name' | count" + json_body="{ - \"query\": \"$table | where timestamp > ago(15m) | where cloud_RoleInstance == '$pod_name' | count\", + \"query\": \"$query\", \"options\": { \"truncationMaxSize\": 67108864 }, @@ -50,28 +71,35 @@ verify_AI_telemetry() { }" echo "Validating $table telemetry for $pod_name ($app_type)..." - response=$(curl -s -X POST $url \ + response=$(curl -s -w "\n%{http_code}" -X POST $url \ -H "Authorization: Bearer $access_token" \ -H "Content-Type: application/json" \ -d "$json_body") - count_val=$(echo $response | jq '.tables[0].rows[0][0]') + http_code=$(echo "$response" | tail -n 1) + response_body=$(echo "$response" | sed '$d') + + count_val=$(echo $response_body | jq '.tables[0].rows[0][0]') if (( count_val > 0 )); then echo "$table telemetry found: $count_val" + found_any=1 else - echo "No $table telemetry found for $pod_name ($app_type)" >&2 + echo "No $table telemetry found for $pod_name ($app_type) [HTTP $http_code]" >&2 echo "Validation for $app_type pods failed: No $table telemetry found" >&2 return 1 fi done } -max_retries=10 -retry_interval=30 +max_retries=30 +retry_interval=10 + +# Convert comma-separated list to array +IFS=',' read -ra APPS_ARRAY <<< "$APPS_TO_VALIDATE" -for app in "java" "nodejs" "python" "dotnet"; do +for app in "${APPS_ARRAY[@]}"; do skip_exceptions="false" if [ "$app" = "java" ]; then pod_name="$POD_JAVA_NAME" @@ -82,6 +110,8 @@ for app in "java" "nodejs" "python" "dotnet"; do elif [ "$app" = "dotnet" ]; then pod_name="$POD_DOTNET_NAME" skip_exceptions="true" + elif [ "$app" = "go" ]; then + pod_name="$POD_GO_NAME" else echo "Unsupported application type: $app" exit 1 diff --git a/appmonitoring/scripts/validate_amw.sh b/appmonitoring/scripts/validate_amw.sh new file mode 100644 index 000000000..165f6fde2 --- /dev/null +++ b/appmonitoring/scripts/validate_amw.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +AMW_QUERY_ENDPOINT=$1 +NAMESPACE=$2 +APPS_TO_VALIDATE=$3 # Comma-separated list of apps (e.g., "java,nodejs,python,dotnet" or "go") +shift 3 # Remove first 3 arguments + +# Validate that required parameters are provided +if [[ -z "$AMW_QUERY_ENDPOINT" ]]; then + echo "Error: AMW_QUERY_ENDPOINT parameter is required (1st argument)" >&2 + echo "Usage: $0 " >&2 + exit 1 +fi + +if [[ -z "$APPS_TO_VALIDATE" ]]; then + echo "Error: APPS_TO_VALIDATE parameter is required (3rd argument)" >&2 + echo "Usage: $0 " >&2 + exit 1 +fi + +echo "Finding pods in namespace: $NAMESPACE for Java App $JAVA_TEST_APP_NAME, NodeJS App $NODEJS_TEST_APP_NAME, Python App $PYTHON_TEST_APP_NAME, Dotnet App $DOTNET_TEST_APP_NAME, and Go App $GO_TEST_APP_NAME" +POD_JAVA_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$JAVA_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) +POD_NODEJS_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$NODEJS_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) +POD_PYTHON_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$PYTHON_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) +POD_DOTNET_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$DOTNET_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) +POD_GO_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=$GO_TEST_APP_NAME --no-headers -o custom-columns=":metadata.name" | head -n 1) + +# Get an access token for Azure Monitor +result_rsp=$(curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://prometheus.monitor.azure.com&mi_res_id=/subscriptions/66010356-d8a5-42d3-8593-6aaa3aeb1c11/resourceGroups/rambhatt-rnd-v2/providers/Microsoft.ManagedIdentity/userAssignedIdentities/rambhatt-agentpool-es-identity' -H Metadata:true -s) +access_token=$(echo $result_rsp | jq -r '.access_token') +client_id=$(echo $result_rsp | jq -r '.client_id') + +echo "==========================================" +echo "ACCESS TOKEN DETAILS:" +echo "Using identity with client_id: $client_id" + +# Decode JWT token (access_token is in format: header.payload.signature) +# Extract the payload (second part) +token_payload=$(echo "$access_token" | cut -d '.' -f 2) + +# Add padding if needed (JWT base64 encoding may not be padded) +padding_length=$((4 - ${#token_payload} % 4)) +if [ $padding_length -ne 4 ]; then + token_payload="${token_payload}$(printf '%*s' $padding_length | tr ' ' '=')" +fi + +# Decode the base64 payload +decoded_token=$(echo "$token_payload" | base64 -d 2>/dev/null) + +echo "Decoded Token Payload:" +echo "$decoded_token" | jq '.' 2>/dev/null || echo "$decoded_token" +echo "==========================================" +echo "" +echo "AMW Query Endpoint: $AMW_QUERY_ENDPOINT" + +verify_amw_metrics() { + local pod_name="$1" + local app_type="$2" + + echo "Validating AMW metrics for $pod_name ($app_type)..." + if [[ -z "$pod_name" ]]; then + echo "Pod name is empty. Validation failed for $app_type pod $pod_name." + exit 1 + fi + + # Query for cows_sold_total metric with specific pod name + # Using Prometheus query syntax for Azure Monitor Workspace + # Using service.instance.id label which typically contains the pod name + # Exclude metrics with sdk.version to ensure we're not looking at metrics forked by Breeze + query="cows_sold_total{\"service.instance.id\"=\"$pod_name\",\"sdk.version\"=\"\"}" + + # Calculate time range (last 15 minutes) + end_time=$(date -u +%s) + start_time=$((end_time - 900)) # 15 minutes = 900 seconds + + echo "==========================================" + echo "REQUEST DETAILS:" + echo "Endpoint: $AMW_QUERY_ENDPOINT/api/v1/query" + echo "Query: $query" + echo "Time: $end_time ($(date -u -d @$end_time +%Y-%m-%dT%H:%M:%SZ))" + echo "Time range: $(date -u -d @$start_time +%Y-%m-%dT%H:%M:%SZ) to $(date -u -d @$end_time +%Y-%m-%dT%H:%M:%SZ)" + echo "Authorization: Bearer " + echo "==========================================" + + # Query the Azure Monitor Workspace for Prometheus metrics + response=$(curl -s -w "\n%{http_code}" -G "$AMW_QUERY_ENDPOINT/api/v1/query" \ + --data-urlencode "query=$query" \ + --data-urlencode "time=$end_time" \ + -H "Authorization: Bearer $access_token" \ + -H "Content-Type: application/json") + + http_code=$(echo "$response" | tail -n 1) + response_body=$(echo "$response" | sed '$d') + + echo "HTTP Status: $http_code" + + if [[ "$http_code" != "200" ]]; then + echo "Failed to query AMW. HTTP Status: $http_code" >&2 + echo "Response: $response_body" >&2 + return 1 + fi + + # Parse the Prometheus response + status=$(echo "$response_body" | jq -r '.status') + + if [[ "$status" != "success" ]]; then + echo "Query failed with status: $status" >&2 + echo "Response: $response_body" >&2 + return 1 + fi + + # Check if we have results + result_type=$(echo "$response_body" | jq -r '.data.resultType') + results_count=$(echo "$response_body" | jq '.data.result | length') + + echo "Result type: $result_type, Results count: $results_count" + + if [[ "$results_count" -eq 0 ]]; then + echo "No cows_sold_total metrics found for pod $pod_name ($app_type)" >&2 + echo "Full response: $response_body" >&2 + return 1 + fi + + # Get the metric value + metric_value=$(echo "$response_body" | jq -r '.data.result[0].value[1]') + metric_labels=$(echo "$response_body" | jq -c '.data.result[0].metric') + + echo "Found cows_sold_total metric for $pod_name ($app_type)" + echo " Value: $metric_value" + echo " Labels: $metric_labels" + + return 0 +} + +max_retries=30 +retry_interval=10 + +# Convert comma-separated list to array +IFS=',' read -ra APPS_ARRAY <<< "$APPS_TO_VALIDATE" + +for app in "${APPS_ARRAY[@]}"; do + if [ "$app" = "java" ]; then + pod_name="$POD_JAVA_NAME" + elif [ "$app" = "nodejs" ]; then + pod_name="$POD_NODEJS_NAME" + elif [ "$app" = "python" ]; then + pod_name="$POD_PYTHON_NAME" + elif [ "$app" = "dotnet" ]; then + pod_name="$POD_DOTNET_NAME" + elif [ "$app" = "go" ]; then + pod_name="$POD_GO_NAME" + else + echo "Unsupported application type: $app" + exit 1 + fi + + attempt=1 + success=0 + while [ $attempt -le $max_retries ]; do + echo "Attempt $attempt/$max_retries: Validating AMW metrics for $pod_name ($app)..." + if verify_amw_metrics "$pod_name" "$app"; then + echo "AMW metrics validation succeeded for $pod_name ($app)" + success=1 + break + else + echo "AMW metrics validation failed for $pod_name ($app) on attempt $attempt" + if [ $attempt -eq $max_retries ]; then + echo "AMW metrics validation failed for $pod_name ($app) after $max_retries attempts" + exit 1 + fi + echo "Waiting $retry_interval seconds before retrying..." + sleep $retry_interval + fi + attempt=$((attempt + 1)) + done +done + +echo "✓ All AMW metrics validation checks passed!" diff --git a/appmonitoring/validation-helm/appmonitoring-cr.yaml b/appmonitoring/validation-helm/appmonitoring-cr.yaml index 1ad2c282b..8f4aba04d 100644 --- a/appmonitoring/validation-helm/appmonitoring-cr.yaml +++ b/appmonitoring/validation-helm/appmonitoring-cr.yaml @@ -9,4 +9,4 @@ spec: - Java - NodeJs destination: # required - applicationInsightsConnectionString: InstrumentationKey=2b453402-fcfb-408f-8495-c551f0e82f46;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=71a68a81-915e-4686-9f4b-eadcfc28689a + applicationInsightsConnectionString: InstrumentationKey=0c12a0a6-a10c-4722-8753-0644d2938d45;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=37743a46-5226-447c-842b-35fac54dbd92 diff --git a/appmonitoring/validation-helm/test-apps/dotnet-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/dotnet-charts/Chart.yaml index 43350ec13..f153e33a6 100644 --- a/appmonitoring/validation-helm/test-apps/dotnet-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/dotnet-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\dotnet-test-app-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: dotnet-test-app description: A Helm chart for .NET test app with OpenTelemetry instrumentation diff --git a/appmonitoring/validation-helm/test-apps/dotnet-charts/templates/deployment.yaml b/appmonitoring/validation-helm/test-apps/dotnet-charts/templates/deployment.yaml index 333b0fe84..53a5664cc 100644 --- a/appmonitoring/validation-helm/test-apps/dotnet-charts/templates/deployment.yaml +++ b/appmonitoring/validation-helm/test-apps/dotnet-charts/templates/deployment.yaml @@ -25,5 +25,7 @@ spec: env: - name: TARGET_URL value: "{{ .Values.targetUrl }}" + - name: OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES + value: "dotnet-test-app" ports: - containerPort: {{ .Values.port }} \ No newline at end of file diff --git a/appmonitoring/validation-helm/test-apps/dotnet-instrumented/Program.cs b/appmonitoring/validation-helm/test-apps/dotnet-instrumented/Program.cs index 49210749a..c1657f065 100644 --- a/appmonitoring/validation-helm/test-apps/dotnet-instrumented/Program.cs +++ b/appmonitoring/validation-helm/test-apps/dotnet-instrumented/Program.cs @@ -3,23 +3,12 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Exporter; +using OpenTelemetry.Trace; using System.Diagnostics; using System.Diagnostics.Metrics; var builder = WebApplication.CreateBuilder(args); -// Configure environment variables programmatically (similar to nodejs instrumentation.js) -Environment.SetEnvironmentVariable("OTEL_SERVICE_NAME", "dotnet-instrumented-test-app"); -Environment.SetEnvironmentVariable("OTEL_SERVICE_VERSION", "1.0.0"); -Environment.SetEnvironmentVariable("OTEL_ENVIRONMENT", "development"); - -// Get configurable endpoint and protocol from environment variables -var metricsEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") ?? "http://localhost:56682/v1/metrics"; -var metricsProtocol = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL") ?? "http/protobuf"; - -Console.WriteLine($"OpenTelemetry Metrics Endpoint: {metricsEndpoint}"); -Console.WriteLine($"OpenTelemetry Metrics Protocol: {metricsProtocol}"); - // Configure services builder.Services.AddHttpClient(); builder.Services.AddControllers(); @@ -29,45 +18,28 @@ .ConfigureResource(resource => { resource.AddService( - serviceName: Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME") ?? "dotnet-instrumented-test-app", - serviceVersion: Environment.GetEnvironmentVariable("OTEL_SERVICE_VERSION") ?? "1.0.0") + serviceName: "dotnet-instrumented-test-app", + serviceVersion: "1.0.0") .AddAttributes(new Dictionary { - ["deployment.environment"] = Environment.GetEnvironmentVariable("OTEL_ENVIRONMENT") ?? "development" + ["deployment.environment"] = "development" }) .AddEnvironmentVariableDetector(); // This automatically handles OTEL_RESOURCE_ATTRIBUTES }) + .WithTracing(tracing => + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }) .WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddMeter("dotnet-instrumented-test-app") - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(metricsEndpoint); - - // Configure protocol based on environment variable - if (metricsProtocol.Equals("grpc", StringComparison.OrdinalIgnoreCase)) - { - options.Protocol = OtlpExportProtocol.Grpc; - Console.WriteLine("Using gRPC protocol for OTLP metrics export"); - } - else if (metricsProtocol.Equals("http/protobuf", StringComparison.OrdinalIgnoreCase)) - { - options.Protocol = OtlpExportProtocol.HttpProtobuf; - Console.WriteLine("Using HTTP/Protobuf protocol for OTLP metrics export"); - } - else - { - Console.WriteLine($"Unsupported OTLP metrics protocol: {metricsProtocol}, defaulting to HTTP/Protobuf"); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - } - - // Export metrics every 5 seconds (similar to nodejs) - options.ExportProcessorType = ExportProcessorType.Batch; - }); - }); + .AddMeter("dotnet-instrumented-test-app"); + }) + .UseOtlpExporter(); // Will use OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL for both traces and metrics var app = builder.Build(); @@ -114,9 +86,7 @@ public IActionResult Get() _httpRequestsTotal.Add(1, labels.ToArray()); _cowsSoldTotal.Add(1, new KeyValuePair[] { - new("cow_type", "Holstein"), - new("endpoint", Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")), - new("protocol", Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")) + new("cow_type", "Holstein .NET Instrumented") }); _logger.LogInformation(".NET instrumented application is running!"); @@ -124,8 +94,8 @@ public IActionResult Get() return Ok(new { message = ".NET instrumented application is running!", timestamp = DateTime.UtcNow.ToString("O"), - service = Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME"), - version = Environment.GetEnvironmentVariable("OTEL_SERVICE_VERSION") + service = "dotnet-instrumented-test-app", + version = "1.0.0" }); } finally @@ -161,7 +131,13 @@ public async Task CallTarget() try { - if (new Random().NextDouble() < 0.4) + // Increment cows sold counter + _cowsSoldTotal.Add(1, new KeyValuePair[] + { + new("cow_type", "Holstein .NET Instrumented") + }); + + if (new Random().NextDouble() < 0.2) { statusCode = 500; throw new Exception("An unexpected error occurred"); diff --git a/appmonitoring/validation-helm/test-apps/dotnet-instrumented/chart.yaml b/appmonitoring/validation-helm/test-apps/dotnet-instrumented/chart.yaml index 929d0742f..1833c2a87 100644 --- a/appmonitoring/validation-helm/test-apps/dotnet-instrumented/chart.yaml +++ b/appmonitoring/validation-helm/test-apps/dotnet-instrumented/chart.yaml @@ -1,4 +1,4 @@ -# http://:3001/generate-load?iterations=5 +# http://:3001/call-target apiVersion: apps/v1 kind: Deployment metadata: @@ -16,7 +16,7 @@ spec: labels: app: dotnet-instrumented-test-app annotations: - #instrumentation.opentelemetry.io/inject-configuration: "true" + instrumentation.opentelemetry.io/inject-configuration: "true" spec: containers: - name: dotnet-instrumented-test-app @@ -33,6 +33,10 @@ spec: value: "Production" - name: ASPNETCORE_URLS value: "http://+:3001" + - name: TARGET_URL + value: "https://bing.com" + - name: OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES + value: "dotnet-instrumented-test-app" ports: - containerPort: 3001 name: http diff --git a/appmonitoring/validation-helm/test-apps/dotnet/dotnet-test-app.cs b/appmonitoring/validation-helm/test-apps/dotnet/dotnet-test-app.cs index 99c301015..35ae13787 100644 --- a/appmonitoring/validation-helm/test-apps/dotnet/dotnet-test-app.cs +++ b/appmonitoring/validation-helm/test-apps/dotnet/dotnet-test-app.cs @@ -1,9 +1,27 @@ using Microsoft.AspNetCore.Mvc; +using System.Diagnostics; +using System.Diagnostics.Metrics; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient(); builder.Services.AddControllers(); +// Create the meter and counter early, before the app builds +var meter = new Meter("dotnet-test-app", "1.0.0"); +var cowsSoldCounter = meter.CreateCounter("cows_sold_total", description: "Total number of cows sold"); + +Console.WriteLine("========================================="); +Console.WriteLine($"Created Meter: {meter.Name} (Version: {meter.Version})"); +Console.WriteLine($"Created Counter: cows_sold_total"); +Console.WriteLine($"OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES: {Environment.GetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES")}"); +Console.WriteLine($"OTEL_METRICS_EXPORTER: {Environment.GetEnvironmentVariable("OTEL_METRICS_EXPORTER")}"); +Console.WriteLine($"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: {Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")}"); +Console.WriteLine("========================================="); + +// Register them as singletons so they can be injected +builder.Services.AddSingleton(meter); +builder.Services.AddSingleton(cowsSoldCounter); + var app = builder.Build(); app.MapControllers(); @@ -16,11 +34,13 @@ public class HomeController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; - - public HomeController(IHttpClientFactory httpClientFactory, ILogger logger) + private readonly Counter _cowsSoldCounter; + + public HomeController(IHttpClientFactory httpClientFactory, ILogger logger, Counter cowsSoldCounter) { _httpClientFactory = httpClientFactory; _logger = logger; + _cowsSoldCounter = cowsSoldCounter; } [HttpGet] @@ -32,6 +52,14 @@ public IActionResult Get() [HttpGet("call-target")] public async Task CallTarget() { + // Increment the cows sold counter + _cowsSoldCounter.Add(1, new KeyValuePair[] + { + new("cow_type", "Holstein .NET"), + new("endpoint", Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")), + new("protocol", Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")) + }); + if (new Random().NextDouble() < 0.4) { throw new Exception("An unexpected error occurred"); diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/Chart.yaml new file mode 100644 index 000000000..b8ee97629 --- /dev/null +++ b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/Chart.yaml @@ -0,0 +1,24 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\go-instrumented-test-app-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps +apiVersion: v2 +name: go-instrumented-test-app +description: A Helm chart for Go test app with OpenTelemetry instrumentation +type: application +version: 0.1.0 +appVersion: "1.0.0" +home: https://github.com/microsoft/Docker-Provider +sources: + - https://github.com/microsoft/Docker-Provider +maintainers: + - name: Microsoft + email: containerinsights@microsoft.com +keywords: + - monitoring + - testing + - go + - opentelemetry +annotations: + category: Testing diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/NOTES.txt b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/NOTES.txt new file mode 100644 index 000000000..da1cc0623 --- /dev/null +++ b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/NOTES.txt @@ -0,0 +1,13 @@ +{{ .Values.appName }} has been deployed! + +To check the service: + kubectl get service {{ .Values.appName }}-service + +To view the logs: + kubectl logs -f deployment/{{ .Values.appName }} + +The app is running on port {{ .Values.port }}. +{{- if .Values.otelInstrumentation }} + +OpenTelemetry auto-instrumentation is enabled (private preview). +{{- end }} diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/deployment.yaml b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/deployment.yaml new file mode 100644 index 000000000..4e6e33b71 --- /dev/null +++ b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.appName }} + labels: + app: {{ .Values.appName }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.appName }} + template: + metadata: + labels: + app: {{ .Values.appName }} + annotations: + instrumentation.opentelemetry.io/inject-configuration: "true" + spec: + containers: + - name: {{ .Values.appName }} + image: "{{ .Values.image }}" + imagePullPolicy: Always + env: + - name: TARGET_URL + value: "{{ .Values.targetUrl }}" + - name: PORT + value: "{{ .Values.port }}" + ports: + - containerPort: {{ .Values.port }} diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/service.yaml b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/service.yaml new file mode 100644 index 000000000..d9b72e58f --- /dev/null +++ b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.appName }}-service + labels: + app: {{ .Values.appName }} +spec: + type: ClusterIP + ports: + - port: {{ .Values.port }} + protocol: TCP + targetPort: {{ .Values.port }} + selector: + app: {{ .Values.appName }} diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented-charts/values.yaml b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/values.yaml new file mode 100644 index 000000000..3ac5118d2 --- /dev/null +++ b/appmonitoring/validation-helm/test-apps/go-instrumented-charts/values.yaml @@ -0,0 +1,9 @@ +# Default values for go-instrumented-test-app +appName: go-instrumented-test-app +image: appmonitoring.azurecr.io/demoaks-go-instrumented-app:latest + +# Port the service runs on +port: 3001 + +# Target URL for the app to call +targetUrl: "http://testappsource-service.default.svc.cluster.local:3001" \ No newline at end of file diff --git a/appmonitoring/validation-helm/test-apps/go-instrumented/main.go b/appmonitoring/validation-helm/test-apps/go-instrumented/main.go index 12803d067..ecb1dd91f 100644 --- a/appmonitoring/validation-helm/test-apps/go-instrumented/main.go +++ b/appmonitoring/validation-helm/test-apps/go-instrumented/main.go @@ -10,18 +10,14 @@ import ( "os" "os/signal" "strconv" - "strings" "syscall" "time" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" @@ -54,14 +50,6 @@ var ( environment = getEnv("OTEL_ENVIRONMENT", "development") port = getEnv("PORT", "3001") targetURL = getEnv("TARGET_URL", "http://localhost:3001/") - - // OTLP configuration - metricsEndpoint = getEnv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:56682") - metricsProtocol = getEnv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "http/protobuf") - tracesEndpoint = getEnv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:56682") - tracesProtocol = getEnv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "http/protobuf") - logsEndpoint = getEnv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://localhost:56682") - logsProtocol = getEnv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "http/protobuf") ) func getEnv(key, defaultValue string) string { @@ -71,23 +59,12 @@ func getEnv(key, defaultValue string) string { return defaultValue } -// initOpenTelemetry initializes OpenTelemetry with configurable OTLP exporters +// initOpenTelemetry initializes OpenTelemetry with OTLP exporters +// The SDK automatically reads OTEL_EXPORTER_OTLP_* environment variables func initOpenTelemetry(ctx context.Context) (*sdkmetric.MeterProvider, *sdktrace.TracerProvider, *sdklog.LoggerProvider, error) { - fmt.Printf("OpenTelemetry Metrics Endpoint: %s\n", metricsEndpoint) - fmt.Printf("OpenTelemetry Metrics Protocol: %s\n", metricsProtocol) - fmt.Printf("OpenTelemetry Traces Endpoint: %s\n", tracesEndpoint) - fmt.Printf("OpenTelemetry Traces Protocol: %s\n", tracesProtocol) - fmt.Printf("OpenTelemetry Logs Endpoint: %s\n", logsEndpoint) - fmt.Printf("OpenTelemetry Logs Protocol: %s\n", logsProtocol) - // Create resource with service information // resource.WithFromEnv() automatically handles OTEL_RESOURCE_ATTRIBUTES res, err := resource.New(ctx, - // resource.WithAttributes( - // //semconv.ServiceName(serviceName), - // //semconv.ServiceVersion(serviceVersion), - // attribute.String("deployment.environment", environment), - // ), resource.WithFromEnv(), // This automatically reads OTEL_RESOURCE_ATTRIBUTES resource.WithProcessPID(), resource.WithProcessExecutableName(), @@ -97,176 +74,56 @@ func initOpenTelemetry(ctx context.Context) (*sdkmetric.MeterProvider, *sdktrace return nil, nil, nil, fmt.Errorf("failed to create resource: %w", err) } - // Initialize Trace Provider - var traceProvider *sdktrace.TracerProvider - - // Create OTLP trace exporter based on protocol - if tracesProtocol == "grpc" { - // For gRPC, we need to remove the http:// prefix and /v1/traces path - endpoint := tracesEndpoint - endpoint = strings.TrimPrefix(endpoint, "http://") - endpoint = strings.TrimPrefix(endpoint, "https://") - endpoint = strings.TrimSuffix(endpoint, "/v1/traces") - - fmt.Printf("Attempting gRPC trace connection to: %s\n", endpoint) - traceExporter, err := otlptracegrpc.New(ctx, - otlptracegrpc.WithEndpoint(endpoint), - otlptracegrpc.WithInsecure(), - ) - if err != nil { - fmt.Printf("Failed to create gRPC OTLP trace exporter: %v\n", err) - fmt.Println("Using no-op trace provider") - traceProvider = sdktrace.NewTracerProvider(sdktrace.WithResource(res)) - } else { - fmt.Println("Using gRPC protocol for OTLP traces export") - traceProvider = sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithBatcher(traceExporter), - ) - } - } else if tracesProtocol == "http/protobuf" { - // For HTTP, remove /v1/traces if present (the exporter adds it automatically) - endpoint := tracesEndpoint - endpoint = strings.TrimSuffix(endpoint, "/v1/traces") - - fmt.Printf("Attempting HTTP trace connection to: %s\n", endpoint) - traceExporter, err := otlptracehttp.New(ctx, - otlptracehttp.WithEndpointURL(endpoint), - otlptracehttp.WithInsecure(), - ) - if err != nil { - fmt.Printf("Failed to create HTTP OTLP trace exporter: %v\n", err) - fmt.Println("Using no-op trace provider") - traceProvider = sdktrace.NewTracerProvider(sdktrace.WithResource(res)) - } else { - fmt.Println("Using HTTP/Protobuf protocol for OTLP traces export") - traceProvider = sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithBatcher(traceExporter), - ) - } - } else { - fmt.Printf("Unsupported OTLP traces protocol: %s, using no-op provider\n", tracesProtocol) - traceProvider = sdktrace.NewTracerProvider(sdktrace.WithResource(res)) + // Initialize Trace Provider with HTTP exporter (SDK reads OTEL_EXPORTER_OTLP_TRACES_* env vars) + traceExporter, err := otlptracehttp.New(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err) } + traceProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithBatcher(traceExporter), + ) + // Set global trace provider otel.SetTracerProvider(traceProvider) - // Try to create OTLP metric exporter, fall back to no-op if it fails - var metricExporter sdkmetric.Exporter - var meterProvider *sdkmetric.MeterProvider - - // Create OTLP metric exporter based on protocol - if metricsProtocol == "grpc" { - // For gRPC, we need to remove the http:// prefix and /v1/metrics path - endpoint := metricsEndpoint - endpoint = strings.TrimPrefix(endpoint, "http://") - endpoint = strings.TrimPrefix(endpoint, "https://") - endpoint = strings.TrimSuffix(endpoint, "/v1/metrics") - - fmt.Printf("Attempting gRPC connection to: %s\n", endpoint) - metricExporter, err = otlpmetricgrpc.New(ctx, - otlpmetricgrpc.WithEndpoint(endpoint), - otlpmetricgrpc.WithInsecure(), - ) - if err != nil { - fmt.Printf("Failed to create gRPC OTLP exporter: %v\n", err) - fmt.Println("Falling back to no-op meter provider") - meterProvider = sdkmetric.NewMeterProvider(sdkmetric.WithResource(res)) - } else { - fmt.Println("Using gRPC protocol for OTLP metrics export") - meterProvider = sdkmetric.NewMeterProvider( - sdkmetric.WithResource(res), - sdkmetric.WithReader(sdkmetric.NewPeriodicReader( - metricExporter, - sdkmetric.WithInterval(5*time.Second), - )), - ) - } - } else if metricsProtocol == "http/protobuf" { - // For HTTP, remove /v1/metrics if present (the exporter adds it automatically) - endpoint := metricsEndpoint - endpoint = strings.TrimSuffix(endpoint, "/v1/metrics") - - fmt.Printf("Attempting HTTP connection to: %s\n", endpoint) - metricExporter, err = otlpmetrichttp.New(ctx, - otlpmetrichttp.WithEndpointURL(endpoint), - otlpmetrichttp.WithInsecure(), - ) - if err != nil { - fmt.Printf("Failed to create HTTP OTLP exporter: %v\n", err) - fmt.Println("Falling back to no-op meter provider") - meterProvider = sdkmetric.NewMeterProvider(sdkmetric.WithResource(res)) - } else { - fmt.Println("Using HTTP/Protobuf protocol for OTLP metrics export") - meterProvider = sdkmetric.NewMeterProvider( - sdkmetric.WithResource(res), - sdkmetric.WithReader(sdkmetric.NewPeriodicReader( - metricExporter, - sdkmetric.WithInterval(5*time.Second), - )), - ) - } - } else { - fmt.Printf("Unsupported OTLP metrics protocol: %s, using no-op provider\n", metricsProtocol) - meterProvider = sdkmetric.NewMeterProvider(sdkmetric.WithResource(res)) + // Initialize Metric Provider with HTTP exporter (SDK reads OTEL_EXPORTER_OTLP_METRICS_* env vars) + metricExporter, err := otlpmetrichttp.New(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err) } + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(sdkmetric.NewPeriodicReader( + metricExporter, + sdkmetric.WithInterval(5*time.Second), + )), + ) + // Set global meter provider otel.SetMeterProvider(meterProvider) // Set global propagator otel.SetTextMapPropagator(propagation.TraceContext{}) - // Initialize Log Provider - var logProvider *sdklog.LoggerProvider - - // Create OTLP log exporter based on protocol - if logsProtocol == "grpc" { - // For gRPC, we need to remove the http:// prefix and add port if needed - endpoint := logsEndpoint - endpoint = strings.TrimPrefix(endpoint, "http://") - endpoint = strings.TrimPrefix(endpoint, "https://") - if !strings.Contains(endpoint, ":") { - endpoint = endpoint + ":4317" - } - - logExporter, err := otlploggrpc.New(ctx, - otlploggrpc.WithEndpoint(endpoint), - otlploggrpc.WithInsecure(), - ) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to create OTLP log gRPC exporter: %w", err) - } - - processor := sdklog.NewBatchProcessor(logExporter) - logProvider = sdklog.NewLoggerProvider( - sdklog.WithProcessor(processor), - sdklog.WithResource(res), - ) - } else { - // Default to HTTP - logExporter, err := otlploghttp.New(ctx, - //otlploghttp.WithEndpointURL(logsEndpoint), - otlploghttp.WithInsecure(), - ) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to create OTLP log HTTP exporter: %w", err) - } - - processor := sdklog.NewBatchProcessor(logExporter) - logProvider = sdklog.NewLoggerProvider( - sdklog.WithProcessor(processor), - sdklog.WithResource(res), - ) - - stdlog.Printf("Logs exporter is set for HTTP. logsEndpoint is %s", logsEndpoint) + // Initialize Log Provider with HTTP exporter (SDK reads OTEL_EXPORTER_OTLP_LOGS_* env vars) + logExporter, err := otlploghttp.New(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create OTLP log exporter: %w", err) } + processor := sdklog.NewBatchProcessor(logExporter) + logProvider := sdklog.NewLoggerProvider( + sdklog.WithProcessor(processor), + sdklog.WithResource(res), + ) + // Set global logger provider global.SetLoggerProvider(logProvider) + fmt.Println("OpenTelemetry initialized successfully with OTLP exporters") return meterProvider, traceProvider, logProvider, nil } @@ -334,8 +191,6 @@ func metricsMiddleware(next http.Handler) http.Handler { // Record cows sold metric (same as nodejs-instrumented) cowsSoldTotal.Add(r.Context(), 1, metric.WithAttributes( attribute.String("cow_type", "Holstein"), - attribute.String("endpoint", metricsEndpoint), - attribute.String("protocol", metricsProtocol), )) // Create a custom span for cow sold tracking @@ -345,8 +200,6 @@ func metricsMiddleware(next http.Handler) http.Handler { // Add custom attributes callSpan.SetAttributes( attribute.String("cow_type", "Holstein"), - attribute.String("endpoint", tracesEndpoint), - attribute.String("protocol", tracesProtocol), ) record := log.Record{} @@ -354,8 +207,6 @@ func metricsMiddleware(next http.Handler) http.Handler { record.SetBody(log.StringValue("cow-sold-once-log")) record.AddAttributes( log.String("cow_type", "Holstein"), - log.String("endpoint", logsEndpoint), - log.String("protocol", logsProtocol), ) logger.Emit(ctx, record) diff --git a/appmonitoring/validation-helm/test-apps/java-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/java-charts/Chart.yaml index ce153da4a..b1320767a 100644 --- a/appmonitoring/validation-helm/test-apps/java-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/java-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\java-test-app-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: java-test-app description: A Helm chart for Java test app diff --git a/appmonitoring/validation-helm/test-apps/nodejs-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/nodejs-charts/Chart.yaml index 417e29a44..35e904763 100644 --- a/appmonitoring/validation-helm/test-apps/nodejs-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/nodejs-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\nodejs-test-app-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: nodejs-test-app description: A Helm chart for Node.js test app with Application Insights logging diff --git a/appmonitoring/validation-helm/test-apps/nodejs/server.js b/appmonitoring/validation-helm/test-apps/nodejs/server.js index 6aea59b58..5796cc17a 100644 --- a/appmonitoring/validation-helm/test-apps/nodejs/server.js +++ b/appmonitoring/validation-helm/test-apps/nodejs/server.js @@ -2,7 +2,7 @@ const express = require('express'); const axios = require('axios'); const winston = require('winston'); -const { metrics } = require('@opentelemetry/api'); +const { metrics, trace, SpanStatusCode } = require('@opentelemetry/api'); const app = express(); const PORT = process.env.PORT || 3001; @@ -29,19 +29,22 @@ const logger = winston.createLogger({ // Endpoint that calls another app's endpoint app.get('/call-target', async (req, res) => { try { - cowsSoldCounter.add(1, { cow_type: 'Holstein', endpoint: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, protocol: process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL }); + cowsSoldCounter.add(1, { cow_type: 'Holstein NodeJs', endpoint: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, protocol: process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL }); - // Occasionally throw an error (40% chance) - if (Math.random() < 0.4) { - logger.error('Simulated error at /call-target'); + // Occasionally simulate an error (20% chance) + if (Math.random() < 0.2) { + const error = new Error('Simulated error - this will be recorded in OTel but not crash the app'); + logger.error(`Simulated error at /call-target: ${error.message}`); - // Throw unhandled exception asynchronously - setImmediate(() => { - throw new Error('Unhandled async error - server should continue'); - }); + // Get the current active span (auto-created by OTel instrumentation) and record the error + const span = trace.getActiveSpan(); + if (span) { + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); + } - // Still respond to the client - res.status(500).json({ message: 'Error triggered asynchronously' }); + // Respond to the client + res.status(500).json({ message: 'Error triggered', error: error.message }); return; } @@ -50,6 +53,14 @@ app.get('/call-target', async (req, res) => { res.json({ message: 'Success', data: response.data }); } catch (error) { logger.error(`Error calling target: ${error.message}`); + + // Record the exception in the active span + const span = trace.getActiveSpan(); + if (span) { + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); + } + res.status(500).json({ message: 'Error calling target', error: error.message }); } }); diff --git a/appmonitoring/validation-helm/test-apps/python-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/python-charts/Chart.yaml index 2300c6ddf..9a285452e 100644 --- a/appmonitoring/validation-helm/test-apps/python-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/python-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\python-test-app-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: python-test-app description: A Helm chart for Python test app diff --git a/appmonitoring/validation-helm/test-apps/python/app.py b/appmonitoring/validation-helm/test-apps/python/app.py index 5ecad55f5..8667fbe3f 100644 --- a/appmonitoring/validation-helm/test-apps/python/app.py +++ b/appmonitoring/validation-helm/test-apps/python/app.py @@ -4,17 +4,35 @@ import logging from flask import Flask, jsonify +from opentelemetry import metrics app = Flask(__name__) logging.basicConfig(level=logging.ERROR) +# Create meter and counter for metrics +meter = metrics.get_meter("python-test-app", "1.0.0") +cows_sold_counter = meter.create_counter( + "cows_sold_total", + description="Total number of cows sold" +) + @app.route('/') def home(): return "Python app is up!" @app.route('/call-target') def call_target(): + # Increment the cows sold counter + cows_sold_counter.add( + 1, + { + "cow_type": "Holstein Python", + "endpoint": os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", ""), + "protocol": os.getenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "") + } + ) + if random.random() < 0.4: # 40% chance of failure try: raise ValueError("Something went wrong!") diff --git a/appmonitoring/validation-helm/test-apps/testappcaller-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/testappcaller-charts/Chart.yaml index f54069347..a805d39b6 100644 --- a/appmonitoring/validation-helm/test-apps/testappcaller-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/testappcaller-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\testappcaller-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: testappcaller description: A Helm chart for the test app caller that periodically calls test applications diff --git a/appmonitoring/validation-helm/test-apps/testappcaller-charts/templates/deployment.yaml b/appmonitoring/validation-helm/test-apps/testappcaller-charts/templates/deployment.yaml index c428f310e..726265a1b 100644 --- a/appmonitoring/validation-helm/test-apps/testappcaller-charts/templates/deployment.yaml +++ b/appmonitoring/validation-helm/test-apps/testappcaller-charts/templates/deployment.yaml @@ -43,5 +43,11 @@ spec: value: "3001" - name: TARGET_DOTNET_PATH value: "/call-target" + - name: TARGET_GO_HOST + value: "{{ .Values.goHost }}" + - name: TARGET_GO_PORT + value: "3001" + - name: TARGET_GO_PATH + value: "/call-target" - name: INTERVAL_MS value: "{{ .Values.intervalMs }}" \ No newline at end of file diff --git a/appmonitoring/validation-helm/test-apps/testappcaller-charts/values.yaml b/appmonitoring/validation-helm/test-apps/testappcaller-charts/values.yaml index 08900bb2b..f6a068a8c 100644 --- a/appmonitoring/validation-helm/test-apps/testappcaller-charts/values.yaml +++ b/appmonitoring/validation-helm/test-apps/testappcaller-charts/values.yaml @@ -7,6 +7,7 @@ javaHost: "java-test-app-service.test-ns.svc.cluster.local" nodejsHost: "nodejs-test-app-service.test-ns.svc.cluster.local" pythonHost: "python-test-app-service.test-ns.svc.cluster.local" dotnetHost: "dotnet-test-app-service.test-ns.svc.cluster.local" +goHost: "go-instrumented-test-app-service.test-ns.svc.cluster.local" # How often to call targets (in milliseconds) intervalMs: 5000 \ No newline at end of file diff --git a/appmonitoring/validation-helm/test-apps/testappcaller/index.js b/appmonitoring/validation-helm/test-apps/testappcaller/index.js index 778aeaa86..de383b629 100644 --- a/appmonitoring/validation-helm/test-apps/testappcaller/index.js +++ b/appmonitoring/validation-helm/test-apps/testappcaller/index.js @@ -24,6 +24,12 @@ const TARGETS = [ host: process.env.TARGET_DOTNET_HOST || 'dotnet-test-app-service.test-ns.svc.cluster.local', port: process.env.TARGET_DOTNET_PORT || 3001, path: process.env.TARGET_DOTNET_PATH || '/call-target', + }, + { + name: 'testapp-go', + host: process.env.TARGET_GO_HOST || 'go-instrumented-test-app-service.test-ns.svc.cluster.local', + port: process.env.TARGET_GO_PORT || 3001, + path: process.env.TARGET_GO_PATH || '/call-target', } ]; diff --git a/appmonitoring/validation-helm/test-apps/testappsource-charts/Chart.yaml b/appmonitoring/validation-helm/test-apps/testappsource-charts/Chart.yaml index 483ac70b6..8e0c50d76 100644 --- a/appmonitoring/validation-helm/test-apps/testappsource-charts/Chart.yaml +++ b/appmonitoring/validation-helm/test-apps/testappsource-charts/Chart.yaml @@ -1,3 +1,8 @@ +# PS: +# $env:HELM_EXPERIMENTAL_OCI = "1" +# helm registry login appmonitoring.azurecr.io --username $(az acr credential show --name appmonitoring --query "username" -o tsv) --password $(az acr credential show --name appmonitoring --query "passwords[0].value" -o tsv) +# helm package . +# helm push .\testappsource-0.1.0.tgz oci://appmonitoring.azurecr.io/helm/testapps apiVersion: v2 name: testappsource description: A Helm chart for the test app source - a simple Node.js HTTP server