diff --git a/azure-pipelines-e2e-integration-tests.yml b/azure-pipelines-e2e-integration-tests.yml index 4f63d6d9..bfd1c4ef 100644 --- a/azure-pipelines-e2e-integration-tests.yml +++ b/azure-pipelines-e2e-integration-tests.yml @@ -7,6 +7,8 @@ trigger: none jobs: - job: "End_to_end_integration_tests" + variables: + ApplicationInsightAgentVersion: 3.4.16 displayName: 'End to end integration tests' strategy: maxParallel: 1 @@ -161,5 +163,8 @@ jobs: ConfluentCloudPassword: $(ConfluentCloudPassword) AzureWebJobsEventGridOutputBindingTopicUriString: $(AzureWebJobsEventGridOutputBindingTopicUriString) AzureWebJobsEventGridOutputBindingTopicKeyString: $(AzureWebJobsEventGridOutputBindingTopicKeyString) + ApplicationInsightAPIKey: $(ApplicationInsightAPIKey) + ApplicationInsightAPPID: $(ApplicationInsightAPPID) + ApplicationInsightAgentVersion: $(ApplicationInsightAgentVersion) displayName: 'Build & Run tests' continueOnError: false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d012b533..1b5440ca 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -78,6 +78,7 @@ jobs: dependsOn: Build variables: buildNumber: $[ dependencies.Build.outputs['output.buildNumber'] ] + ApplicationInsightAgentVersion: 3.4.16 strategy: maxParallel: 1 matrix: @@ -224,6 +225,9 @@ jobs: ConfluentCloudPassword: $(ConfluentCloudPassword) AzureWebJobsEventGridOutputBindingTopicUriString: $(AzureWebJobsEventGridOutputBindingTopicUriString) AzureWebJobsEventGridOutputBindingTopicKeyString: $(AzureWebJobsEventGridOutputBindingTopicKeyString) + ApplicationInsightAPIKey: $(ApplicationInsightAPIKey) + ApplicationInsightAPPID: $(ApplicationInsightAPPID) + ApplicationInsightAgentVersion: $(ApplicationInsightAgentVersion) displayName: 'Build & Run tests' continueOnError: false diff --git a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Constants.cs b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Constants.cs index 7fe444db..fb82b718 100644 --- a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Constants.cs +++ b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Constants.cs @@ -87,6 +87,11 @@ public static class Constants public static string ServiceBusConnectionStringSetting = Environment.GetEnvironmentVariable("AzureWebJobsServiceBus"); // Xunit Fixtures and Collections - public const string FunctionAppCollectionName = "FunctionAppCollection"; + public const string FunctionAppCollectionName = "FunctionAppCollection"; + + // Application Insights + public static string ApplicationInsightAPIKey = Environment.GetEnvironmentVariable("ApplicationInsightAPIKey"); + public static string ApplicationInsightAPPID = Environment.GetEnvironmentVariable("ApplicationInsightAPPID"); + public static string ApplicationInsightAgentVersion = Environment.GetEnvironmentVariable("ApplicationInsightAgentVersion"); } } diff --git a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/AppInsightHelper.cs b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/AppInsightHelper.cs new file mode 100644 index 00000000..5cd29bdc --- /dev/null +++ b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/AppInsightHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Threading.Tasks; +using E2ETestCases.Utils; +using Xunit; + +namespace Azure.Functions.Java.Tests.E2E.Helpers.AppInsight +{ + public class AppInsightHelper + { + private const string QueryEndpoint = "https://api.applicationinsights.io/v1/apps/{0}/query?{1}"; + // Trace data will be triggered as soon as node/java app starts. + // Therefore, larger timespan is used here in case traces data will be tested last. + // 15min is estimated on test suite max timeout plus some time buffer. + private const string TracesParameter = "query=traces%7C%20where%20timestamp%20%20%3E%20ago(20min)%7C%20order%20by%20timestamp%20desc%20"; + + public static bool ValidateData(QueryType queryType, String currentSdkVersion) + { + int loopCount = 1; + List data = QueryAzureMonitorTelemetry(queryType).Result; + + while (data == null || data.Count == 0) + { + if (loopCount > 0) + { + // Waiting for 60s for traces showing up in application insight portal + // TODO: is there a more elegant way to wait? + System.Threading.Thread.Sleep(60*1000); + loopCount--; + } + else { + throw new Exception("No Application Insights telemetry available"); + } + } + bool dataFound = false; + foreach (QueryResultRow row in data) + { + if (row.sdkVersion.IndexOf(currentSdkVersion) >= 0) + { + dataFound = true; + break; + // TODO: Add extra checks when test apps generate similar telemetry, validate SDK version only for now + } + } + return dataFound; + } + + public static async Task> QueryAzureMonitorTelemetry(QueryType queryType, bool isInitialCheck = false) + { + List aiResult; + Func, bool> retryCheck = (result) => result == null; + string queryParemeter = ""; + switch (queryType) + { + //TODO: test other type of data as well, for now only test trace. + //case QueryType.exceptions: + // queryParemeter = ExceptionsParameter; + // break; + //case QueryType.dependencies: + // queryParemeter = DependenciesParameter; + // break; + //case QueryType.requests: + // queryParemeter = RequestParameter; + // break; + case QueryType.traces: + queryParemeter = TracesParameter; + break; + //case QueryType.statsbeat: + // queryParemeter = StatsbeatParameter; + // break; + } + + aiResult = await QueryMonitorLogWithRestApi(queryType, queryParemeter); + return aiResult; + } + + private static async Task> QueryMonitorLogWithRestApi(QueryType queryType, string parameterString) + { + try + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var apiKeyVal = Constants.ApplicationInsightAPIKey; + var appIdVal = Constants.ApplicationInsightAPPID; + + client.DefaultRequestHeaders.Add("x-api-key", apiKeyVal); + var req = string.Format(QueryEndpoint, appIdVal, parameterString); + var table = await GetHttpResponse(client, req); + return GetJsonObjectFromQuery(table); + } + catch (Exception ex) + { + Console.WriteLine("Failed to query Azure Motinor Ex:" + ex.Message); + } + return null; + } + + // Get http request response + public static async Task GetHttpResponse(HttpClient client, string url) + { + Uri uri = new Uri(url); + HttpResponseMessage response = null; + try + { + response = client.GetAsync(uri).Result; + } + catch (Exception e) + { + Console.WriteLine("GetXhrResponse Error " + e.Message); return null; + } + if (response == null) + { + Console.WriteLine("GetXhrResponse Error: No Response"); return null; + } + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"Response failed. Status code {response.StatusCode}"); + } + return await response.Content.ReadAsStringAsync(); + } + + // Get latest query object with RestApiJsonSerializeRow format + // prerequisite: query parameter is ordered by timestamp desc + private static List GetJsonObjectFromQuery(string table) + { + if (!(table?.Length > 0)) { return null; } + RestApiJsonSerializeTable SerializeTable = Newtonsoft.Json.JsonConvert.DeserializeObject(table); + if (!(SerializeTable?.Tables?.Count > 0)) { return null; } + List kustoObjectList = new List(); + List columnObjects = SerializeTable.Tables[0].Columns; + List rows = SerializeTable.Tables[0].Rows; + if (!(rows?.Count > 0)) { return null; } + foreach (string[] row in rows) + { + QueryResultRow item = new QueryResultRow(columnObjects, row); + kustoObjectList.Add(item); + } + return kustoObjectList; + } + } +} diff --git a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/Enums.cs b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/Enums.cs new file mode 100644 index 00000000..6242ffc6 --- /dev/null +++ b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/Enums.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace E2ETestCases.Utils +{ + // Query result types + public enum QueryType + { requests, dependencies, exceptions, traces, statsbeat } +} diff --git a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/RestApiJsonSerialize.cs b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/RestApiJsonSerialize.cs new file mode 100644 index 00000000..e3358edc --- /dev/null +++ b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/Helpers/AppInsight/RestApiJsonSerialize.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; + +namespace E2ETestCases.Utils +{ + public class RestApiJsonSerializeCols + { + public string Name { get; set; } + public string Type { get; set; } + } + + public class RestApiJsonSerializeObject + { + public string Name { get; set; } + public List Columns { get; set; } + public List Rows { get; set; } + } + + public class RestApiJsonSerializeTable + { + public List Tables { get; set; } + } + + //TODO: add unit tests + public class QueryResultRow + { + Dictionary data = new Dictionary(); + public QueryResultRow(List cols, string[] rows) + { + for (int i = 0; i < rows.Length; i++) + { + data.Add(cols[i].Name, rows[i]); + } + } + // Here are all fields might be used by Monitor log query results. + // Only those fields in Dictionary(data) will be parsed. + public string timestamp { get => getData("timestamp"); } + public string id { get => getData("id"); } + public string source { get => getData("source"); } + public string name { get => getData("name"); } + public string url { get => getData("url"); } + public string target { get => getData("target"); } + public string success { get => getData("success"); } + public string resultCode { get => getData("resultCode"); } + public string duration { get => getData("duration"); } + public string performanceBucket { get => getData("performanceBucket"); } + public string itemType { get => getData("itemType"); } + public dynamic customDimensions { get => getData("customDimensions"); } + public dynamic customMeasurements { get => getData("customMeasurements"); } + public string operation_Name { get => getData("operation_Name"); } + public string operation_Id { get => getData("operation_Id"); } + public string operation_ParentId { get => getData("operation_ParentId"); } + public string operation_SyntheticSource { get => getData("operation_SyntheticSource"); } + public string session_Id { get => getData("session_Id"); } + public string user_Id { get => getData("user_Id"); } + public string user_AuthenticatedId { get => getData("user_AuthenticatedId"); } + public string user_AccountId { get => getData("user_AccountId"); } + public string application_Version { get => getData("application_Version"); } + public string client_Type { get => getData("client_Type"); } + public string client_Model { get => getData("client_Model"); } + public string client_OS { get => getData("client_OS"); } + public string client_IP { get => getData("client_IP"); } + public string client_City { get => getData("client_City"); } + public string client_StateOrProvince { get => getData("client_StateOrProvince"); } + public string client_CountryOrRegion { get => getData("client_CountryOrRegion"); } + public string client_Browser { get => getData("client_Browser"); } + public string cloud_RoleName { get => getData("cloud_RoleName"); } + public string cloud_RoleInstance { get => getData("cloud_RoleInstance"); } + public string appId { get => getData("appId"); } + public string appName { get => getData("appName"); } + public string iKey { get => getData("iKey"); } + public string sdkVersion { get => getData("sdkVersion"); } + public string itemId { get => getData("itemId"); } + public string itemCount { get => getData("itemCount"); } + public string _ResourceId { get => getData("_ResourceId"); } + public string problemId { get => getData("problemId"); } + public string handledAt { get => getData(" handledAt"); } + public string type { get => getData(" type"); } + public string message { get => getData("message"); } + public string assembly { get => getData("assembly"); } + public string method { get => getData("method"); } + public string outerType { get => getData("outerType"); } + public string outerMessage { get => getData("outerMessage"); } + public string outerAssembly { get => getData("outerAssembly"); } + public string outerMethod { get => getData("outerMethod"); } + public string innermostType { get => getData("innermostType"); } + public string innermostMessage { get => getData("innermostMessage"); } + public string innermostAssembly { get => getData("innermostAssembly"); } + public string innermostMethod { get => getData("innermostMethod"); } + public string severityLevel { get => getData("severityLevel"); } + public string details { get => getData("details"); } + + private string getData(string name) + { + return data.ContainsKey(name) ? data[name] : null; + } + } +} + + diff --git a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/HttpEndToEndTests.cs b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/HttpEndToEndTests.cs index 480be1d8..ff389178 100644 --- a/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/HttpEndToEndTests.cs +++ b/endtoendtests/Azure.Functions.Java.Tests.E2E/Azure.Functions.Java.Tests.E2E/HttpEndToEndTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using Azure.Functions.Java.Tests.E2E.Helpers.AppInsight; using System; using System.Net; using System.Threading.Tasks; @@ -66,5 +67,11 @@ public async Task HttpTrigger_BindingName() { Assert.True(await Utilities.InvokeHttpTrigger("BindingName", "/testMessage", HttpStatusCode.OK, "testMessage")); } + + [Fact] + public void ApplicationInsightTest() + { + Assert.True(AppInsightHelper.ValidateData(E2ETestCases.Utils.QueryType.traces, Constants.ApplicationInsightAgentVersion)); + } } } diff --git a/setup-tests-pipeline.ps1 b/setup-tests-pipeline.ps1 index b5a17e78..6aa28efc 100644 --- a/setup-tests-pipeline.ps1 +++ b/setup-tests-pipeline.ps1 @@ -57,7 +57,7 @@ if (-not $UseCoreToolsBuildFromIntegrationTests.IsPresent) Write-Host "Copying worker.config.json to worker directory" Copy-Item "$PSScriptRoot/worker.config.json" "$FUNC_CLI_DIRECTORY/workers/java" -Force -Verbose Write-Host "Copying worker.config.json and annotationLib to worker directory" - Copy-Item "$PSScriptRoot/annotationLib" "$FUNC_CLI_DIRECTORY/workers/java/annotationLib" -Recurse -Verbose + Copy-Item "$PSScriptRoot/annotationLib" "$FUNC_CLI_DIRECTORY/workers/java" -Recurse -Verbose -Force # Download application insights agent from maven central $ApplicationInsightsAgentFile = [System.IO.Path]::Combine($PSScriptRoot, $ApplicationInsightsAgentFilename) @@ -136,5 +136,5 @@ if (-not $UseCoreToolsBuildFromIntegrationTests.IsPresent) New-Item -path $PSScriptRoot\agent -type file -name "functions.codeless" Write-Host "Copying the unsigned Application Insights Agent to worker directory" - Copy-Item "$PSScriptRoot/agent" "$FUNC_CLI_DIRECTORY/workers/java/agent" -Recurse -Verbose + Copy-Item "$PSScriptRoot/agent" "$FUNC_CLI_DIRECTORY/workers/java" -Recurse -Verbose -Force }