diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/DistroWebAppLiveTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/DistroWebAppLiveTests.cs index 151dac2923f8..b27be2ad51cb 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/DistroWebAppLiveTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/DistroWebAppLiveTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; @@ -16,6 +18,7 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +using static Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests.TelemetryValidationHelper; #if NET6_0_OR_GREATER namespace Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests @@ -38,9 +41,18 @@ public DistroWebAppLiveTests(bool isAsync) : base(isAsync) { } [RecordedTest] [SyncOnly] // This test cannot run concurrently with another test because OTel instruments the process and will cause side effects. - //[Ignore("Test fails in Mac-OS.")] public async Task VerifyDistro() { + Console.WriteLine($"Integration test '{nameof(VerifyDistro)}' running in mode '{TestEnvironment.Mode}'"); + + // DEVELOPER TIP: This test implicitly checks for telemetry within the last 30 minutes. + // When working locally, this has the benefit of "priming" telemetry so that additional runs can complete faster without waiting for ingestion. + // This can negatively impact the test results if you are debugging locally and making changes to the telemetry. + // To mitigate this, you can include a timestamp in the query to only check for telemetry created since this test started. + // IMPORTANT: we cannot include timestamps in the Recorded test because it breaks queries during playback. + // C#: var testStartTimeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"); + // QUERY: | where TimeGenerated >= datetime({ testStartTimeStamp}) + // SETUP TELEMETRY CLIENT (FOR QUERYING LOG ANALYTICS) _logsQueryClient = InstrumentClient(new LogsQueryClient( TestEnvironment.LogsEndpoint, @@ -73,6 +85,7 @@ public async Task VerifyDistro() .ConfigureResource(x => x.AddAttributes(resourceAttributes)) .UseAzureMonitor(options => { + options.EnableLiveMetrics = false; options.ConnectionString = TestEnvironment.ConnectionString; }); @@ -104,41 +117,79 @@ public async Task VerifyDistro() // ASSERT // NOTE: The following queries are using the LogAnalytics schema. - // TODO: NEED TO PERFORM COLUMN LEVEL VALIDATIONS. - await VerifyTelemetry( + await QueryAndVerifyDependency( description: "Dependency for invoking HttpClient, from testhost", - query: $"AppDependencies | where Data == '{TestServerUrl}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated"); + query: $"AppDependencies | where Data == '{TestServerUrl}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated", + expectedAppDependency: new ExpectedAppDependency + { + Data = TestServerUrl, + AppRoleName = RoleName, + }); - await VerifyTelemetry( + await QueryAndVerifyRequest( description: "RequestTelemetry, from WebApp", - query: $"AppRequests | where Url == '{TestServerUrl}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated"); + query: $"AppRequests | where Url == '{TestServerUrl}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated", + expectedAppRequest: new ExpectedAppRequest + { + Url = TestServerUrl, + AppRoleName = RoleName, + }); - await VerifyTelemetry( + await QueryAndVerifyMetric( description: "Metric for outgoing request, from testhost", - query: $"AppMetrics | where Name == 'http.client.duration' | where AppRoleName == '{RoleName}' | where Properties.['net.peer.name'] == 'localhost' | top 1 by TimeGenerated"); + query: $"AppMetrics | where Name == 'http.client.request.duration' | where AppRoleName == '{RoleName}' | where Properties.['server.address'] == 'localhost' | top 1 by TimeGenerated", + expectedAppMetric: new ExpectedAppMetric + { + Name = "http.client.request.duration", + AppRoleName = RoleName, + Properties = new List> + { + new("server.address", "localhost"), + }, + }); - await VerifyTelemetry( + await QueryAndVerifyMetric( description: "Metric for incoming request, from WebApp", - query: $"AppMetrics | where Name == 'http.server.duration' | where AppRoleName == '{RoleName}' | where Properties.['net.host.name'] == 'localhost' | top 1 by TimeGenerated"); + query: $"AppMetrics | where Name == 'http.server.request.duration' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated", + expectedAppMetric: new ExpectedAppMetric + { + Name = "http.server.request.duration", + AppRoleName = RoleName, + Properties = new(), + }); - await VerifyTelemetry( + await QueryAndVerifyTrace( description: "ILogger LogInformation, from WebApp", - query: $"AppTraces | where Message == '{LogMessage}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated"); + query: $"AppTraces | where Message == '{LogMessage}' | where AppRoleName == '{RoleName}' | top 1 by TimeGenerated", + expectedAppTrace: new ExpectedAppTrace + { + Message = LogMessage, + AppRoleName = RoleName, + }); } - private async Task VerifyTelemetry(string description, string query) + private async Task QueryAndVerifyDependency(string description, string query, ExpectedAppDependency expectedAppDependency) { - LogsTable? table = await _logsQueryClient!.CheckForRecordAsync(query); + LogsTable? logsTable = await _logsQueryClient!.QueryTelemetryAsync(description, query); + ValidateExpectedTelemetry(description, logsTable, expectedAppDependency); + } - var rowCount = table?.Rows.Count; - if (rowCount == null || rowCount == 0) - { - Assert.Fail($"No telemetry records were found: {description}"); - } - else - { - Assert.Pass(); - } + private async Task QueryAndVerifyRequest(string description, string query, ExpectedAppRequest expectedAppRequest) + { + LogsTable? logsTable = await _logsQueryClient!.QueryTelemetryAsync(description, query); + ValidateExpectedTelemetry(description, logsTable, expectedAppRequest); + } + + private async Task QueryAndVerifyMetric(string description, string query, ExpectedAppMetric expectedAppMetric) + { + LogsTable? logsTable = await _logsQueryClient!.QueryTelemetryAsync(description, query); + ValidateExpectedTelemetry(description, logsTable, expectedAppMetric); + } + + private async Task QueryAndVerifyTrace(string description, string query, ExpectedAppTrace expectedAppTrace) + { + LogsTable? logsTable = await _logsQueryClient!.QueryTelemetryAsync(description, query); + ValidateExpectedTelemetry(description, logsTable, expectedAppTrace); } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/LogsQueryClientExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/LogsQueryClientExtensions.cs index 6488233627c4..b95487ab4d9a 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/LogsQueryClientExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/LogsQueryClientExtensions.cs @@ -2,47 +2,46 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Threading.Tasks; using Azure.Monitor.Query; using Azure.Monitor.Query.Models; +using NUnit.Framework; namespace Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests { internal static class LogsQueryClientExtensions { private static string s_workspaceId = string.Empty; + private static TimeSpan s_queryDelay = TimeSpan.FromSeconds(30); public static void SetQueryWorkSpaceId(this LogsQueryClient client, string workspaceId) => s_workspaceId = workspaceId; - public static async Task CheckForRecordAsync(this LogsQueryClient client, string query) + public static async Task QueryTelemetryAsync(this LogsQueryClient client, string description, string query) { - LogsTable? table = null; - int count = 0; + Debug.WriteLine($"UnitTest: Query Telemetry ({description})"); + TestContext.Out.WriteLine($"Query Telemetry ({description})"); // Try every 30 secs for total of 5 minutes. int maxTries = 10; - while (count == 0 && maxTries > 0) + for (int attempt = 1; attempt <= maxTries; attempt++) { Response response = await client.QueryWorkspaceAsync( s_workspaceId, query, new QueryTimeRange(TimeSpan.FromMinutes(30))); - table = response.Value.Table; - - count = table.Rows.Count; - - if (count > 0) + if (response.Value.Table.Rows.Count > 0) { - break; + return response.Value.Table; } - maxTries--; - - await Task.Delay(TimeSpan.FromSeconds(30)); + Debug.WriteLine($"UnitTest: Query attempt {attempt}/{maxTries} returned no records. Waiting {s_queryDelay.TotalSeconds} seconds..."); + TestContext.Out.WriteLine($"Query attempt {attempt}/{maxTries} returned no records. Waiting {s_queryDelay.TotalSeconds} seconds..."); + await Task.Delay(s_queryDelay); } - return table; + return null; } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/SessionRecords/DistroWebAppLiveTests/VerifyDistro.json b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/SessionRecords/DistroWebAppLiveTests/VerifyDistro.json index 3b4c4590a334..aa039e05837a 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/SessionRecords/DistroWebAppLiveTests/VerifyDistro.json +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/SessionRecords/DistroWebAppLiveTests/VerifyDistro.json @@ -1,20 +1,20 @@ { "Entries": [ { - "RequestUri": "https://api.loganalytics.io/v1/workspaces/33283218-aeb0-4388-b0c0-77a9bf80a8d2/query", + "RequestUri": "https://api.loganalytics.io/v1/workspaces/d49041ed-21aa-42c5-a6f3-6d60bb93d63c/query", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json", "Authorization": "Sanitized", "Content-Length": "179", "Content-Type": "application/json", - "traceparent": "00-96c190206c9672f1717ab6c6ef2aecd7-2e8e334b521fb0e0-00", - "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.5; Microsoft Windows 10.0.22621)", - "x-ms-client-request-id": "28a87c500b38f9446fbd8fd6b0e14494", + "traceparent": "00-c4516adafd33bcee5da05c3bcb37dd40-938a217833252e6f-00", + "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.20; Microsoft Windows 10.0.22631)", + "x-ms-client-request-id": "Sanitized", "x-ms-return-client-request-id": "true" }, "RequestBody": { - "query": "AppDependencies | where Data == \u0027http://localhost:9998/\u0027 | where AppRoleName == \u0027DistroWebAppLiveTests\u0027 | top 1 by TimeGenerated", + "query": "AppDependencies | where Data == 'http://localhost:9998/' | where AppRoleName == 'DistroWebAppLiveTests' | top 1 by TimeGenerated", "timespan": "PT30M" }, "StatusCode": 200, @@ -22,12 +22,12 @@ "Access-Control-Allow-Origin": "*", "Access-Control-Expose-Headers": "Retry-After,Age,WWW-Authenticate,x-resource-identities,x-ms-status-location", "Connection": "keep-alive", - "Content-Length": "2436", + "Content-Length": "2423", "Content-Type": "application/json; charset=utf-8", - "Date": "Wed, 24 May 2023 20:39:02 GMT", + "Date": "Fri, 07 Jun 2024 21:44:57 GMT", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", "Vary": "Accept-Encoding", - "Via": "1.1 draft-oms-65c7bccb55-g79dl", + "Via": "1.1 draft-oms-84fccbf47-9zf5l", "X-Content-Type-Options": "nosniff" }, "ResponseBody": { @@ -202,22 +202,22 @@ ], "rows": [ [ - "33283218-aeb0-4388-b0c0-77a9bf80a8d2", - "2023-05-24T20:21:02.429781Z", - "f9452d1aabcf2dd7", + "d49041ed-21aa-42c5-a6f3-6d60bb93d63c", + "2024-06-07T21:44:23.1556457Z", + "2f64853161967288", "localhost:9998", "HTTP", "GET /", "http://localhost:9998/", true, "200", - 11.628, - "\u003C250ms", - "{\u0022http.flavor\u0022:\u00221.1\u0022,\u0022_MS.ProcessedByMetricExtractors\u0022:\u0022(Name: X,Ver:\u00271.1\u0027)\u0022}", + 81.8636, + "<250ms", + "{\"_MS.ProcessedByMetricExtractors\":\"(Name: X,Ver:'1.1')\"}", null, "", - "bc260bdd4f27f54a7d4d1270a26d6d09", - "bc260bdd4f27f54a7d4d1270a26d6d09", + "2f5dc72a16261a3a9e74e74ca0f3b4bf", + "2f5dc72a16261a3a9e74e74ca0f3b4bf", "", "", "", @@ -225,24 +225,905 @@ "", "", "DistroWebAppLiveTests", - "Mac-1684957586475.local", + "af7c72ee-94a1-42f8-8306-395ce9147b22", "PC", "Other", + "Windows 10", + "0.0.0.0", + "Quincy", + "Washington", + "United States", + "Other", + "8df423be-a013-477c-88b9-8186722e3d49", + "7b1ad079-51c6-4824-91c1-9decb485ba64", + "dotnet7.0.20:otel1.8.1:ext1.3.0-beta.2-d", + 1, + "", + "", + "Azure", + "AppDependencies", + "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-tileemonitor/providers/microsoft.insights/components/t48da106dbe0b9268" + ] + ] + } + ] + } + }, + { + "RequestUri": "https://api.loganalytics.io/v1/workspaces/d49041ed-21aa-42c5-a6f3-6d60bb93d63c/query", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "174", + "Content-Type": "application/json", + "traceparent": "00-d90e7a2bf246528d5c75c004752cfefc-25dd917267728ea9-00", + "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.20; Microsoft Windows 10.0.22631)", + "x-ms-client-request-id": "Sanitized", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "query": "AppRequests | where Url == 'http://localhost:9998/' | where AppRoleName == 'DistroWebAppLiveTests' | top 1 by TimeGenerated", + "timespan": "PT30M" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "Retry-After,Age,WWW-Authenticate,x-resource-identities,x-ms-status-location", + "Connection": "keep-alive", + "Content-Length": "2392", + "Content-Type": "application/json; charset=utf-8", + "Date": "Fri, 07 Jun 2024 21:44:58 GMT", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Vary": "Accept-Encoding", + "Via": "1.1 draft-oms-84fccbf47-vv6qb", + "X-Content-Type-Options": "nosniff" + }, + "ResponseBody": { + "tables": [ + { + "name": "PrimaryResult", + "columns": [ + { + "name": "TenantId", + "type": "string" + }, + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "Id", + "type": "string" + }, + { + "name": "Source", + "type": "string" + }, + { + "name": "Name", + "type": "string" + }, + { + "name": "Url", + "type": "string" + }, + { + "name": "Success", + "type": "bool" + }, + { + "name": "ResultCode", + "type": "string" + }, + { + "name": "DurationMs", + "type": "real" + }, + { + "name": "PerformanceBucket", + "type": "string" + }, + { + "name": "Properties", + "type": "dynamic" + }, + { + "name": "Measurements", + "type": "dynamic" + }, + { + "name": "OperationName", + "type": "string" + }, + { + "name": "OperationId", + "type": "string" + }, + { + "name": "OperationLinks", + "type": "dynamic" + }, + { + "name": "ParentId", + "type": "string" + }, + { + "name": "SyntheticSource", + "type": "string" + }, + { + "name": "SessionId", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "UserAuthenticatedId", + "type": "string" + }, + { + "name": "UserAccountId", + "type": "string" + }, + { + "name": "AppVersion", + "type": "string" + }, + { + "name": "AppRoleName", + "type": "string" + }, + { + "name": "AppRoleInstance", + "type": "string" + }, + { + "name": "ClientType", + "type": "string" + }, + { + "name": "ClientModel", + "type": "string" + }, + { + "name": "ClientOS", + "type": "string" + }, + { + "name": "ClientIP", + "type": "string" + }, + { + "name": "ClientCity", + "type": "string" + }, + { + "name": "ClientStateOrProvince", + "type": "string" + }, + { + "name": "ClientCountryOrRegion", + "type": "string" + }, + { + "name": "ClientBrowser", + "type": "string" + }, + { + "name": "ResourceGUID", + "type": "string" + }, + { + "name": "IKey", + "type": "string" + }, + { + "name": "SDKVersion", + "type": "string" + }, + { + "name": "ItemCount", + "type": "int" + }, + { + "name": "ReferencedItemId", + "type": "string" + }, + { + "name": "ReferencedType", + "type": "string" + }, + { + "name": "SourceSystem", + "type": "string" + }, + { + "name": "Type", + "type": "string" + }, + { + "name": "_ResourceId", + "type": "string" + } + ], + "rows": [ + [ + "d49041ed-21aa-42c5-a6f3-6d60bb93d63c", + "2024-06-07T21:44:23.2035048Z", + "52f6536389172c57", + "", + "GET /", + "http://localhost:9998/", + true, + "200", + 33.8512, + "<250ms", + "{\"_MS.ProcessedByMetricExtractors\":\"(Name: X,Ver:'1.1')\"}", + null, + "GET /", + "2f5dc72a16261a3a9e74e74ca0f3b4bf", + null, + "2f64853161967288", + "", + "", + "", + "", + "", + "", + "DistroWebAppLiveTests", + "af7c72ee-94a1-42f8-8306-395ce9147b22", + "PC", + "Other", + "Windows 10", + "0.0.0.0", + "Quincy", + "Washington", + "United States", + "Other", + "8df423be-a013-477c-88b9-8186722e3d49", + "7b1ad079-51c6-4824-91c1-9decb485ba64", + "dotnet7.0.20:otel1.8.1:ext1.3.0-beta.2-d", + 1, + "", + "", + "Azure", + "AppRequests", + "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-tileemonitor/providers/microsoft.insights/components/t48da106dbe0b9268" + ] + ] + } + ] + } + }, + { + "RequestUri": "https://api.loganalytics.io/v1/workspaces/d49041ed-21aa-42c5-a6f3-6d60bb93d63c/query", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "253", + "Content-Type": "application/json", + "traceparent": "00-cf77b2398e6b1a6ebf333d08dae5624f-5b6a30caa64b9b65-00", + "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.20; Microsoft Windows 10.0.22631)", + "x-ms-client-request-id": "Sanitized", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "query": "AppMetrics | where Name == 'http.client.request.duration' | where AppRoleName == 'DistroWebAppLiveTests' | where Properties.['server.address'] == 'localhost' | top 1 by TimeGenerated", + "timespan": "PT30M" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "Retry-After,Age,WWW-Authenticate,x-resource-identities,x-ms-status-location", + "Connection": "keep-alive", + "Content-Length": "2097", + "Content-Type": "application/json; charset=utf-8", + "Date": "Fri, 07 Jun 2024 21:44:58 GMT", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Vary": "Accept-Encoding", + "Via": "1.1 draft-oms-84fccbf47-9ff58", + "X-Content-Type-Options": "nosniff" + }, + "ResponseBody": { + "tables": [ + { + "name": "PrimaryResult", + "columns": [ + { + "name": "TenantId", + "type": "string" + }, + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "Name", + "type": "string" + }, + { + "name": "ItemCount", + "type": "int" + }, + { + "name": "Sum", + "type": "real" + }, + { + "name": "Min", + "type": "real" + }, + { + "name": "Max", + "type": "real" + }, + { + "name": "Properties", + "type": "dynamic" + }, + { + "name": "OperationName", + "type": "string" + }, + { + "name": "OperationId", + "type": "string" + }, + { + "name": "ParentId", + "type": "string" + }, + { + "name": "SyntheticSource", + "type": "string" + }, + { + "name": "SessionId", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "UserAuthenticatedId", + "type": "string" + }, + { + "name": "UserAccountId", + "type": "string" + }, + { + "name": "AppVersion", + "type": "string" + }, + { + "name": "AppRoleName", + "type": "string" + }, + { + "name": "AppRoleInstance", + "type": "string" + }, + { + "name": "ClientType", + "type": "string" + }, + { + "name": "ClientModel", + "type": "string" + }, + { + "name": "ClientOS", + "type": "string" + }, + { + "name": "ClientIP", + "type": "string" + }, + { + "name": "ClientCity", + "type": "string" + }, + { + "name": "ClientStateOrProvince", + "type": "string" + }, + { + "name": "ClientCountryOrRegion", + "type": "string" + }, + { + "name": "ClientBrowser", + "type": "string" + }, + { + "name": "ResourceGUID", + "type": "string" + }, + { + "name": "IKey", + "type": "string" + }, + { + "name": "SDKVersion", + "type": "string" + }, + { + "name": "SourceSystem", + "type": "string" + }, + { + "name": "Type", + "type": "string" + }, + { + "name": "_ResourceId", + "type": "string" + } + ], + "rows": [ + [ + "d49041ed-21aa-42c5-a6f3-6d60bb93d63c", + "2024-06-07T21:44:23.41758Z", + "http.client.request.duration", + 1, + 0.0818636, + 0.0818636, + 0.0818636, + "{\"http.request.method\":\"GET\",\"http.response.status_code\":\"200\",\"network.protocol.version\":\"1.1\",\"server.address\":\"localhost\",\"server.port\":\"9998\",\"url.scheme\":\"http\"}", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "DistroWebAppLiveTests", + "af7c72ee-94a1-42f8-8306-395ce9147b22", + "PC", + "Other", + "Windows 10", + "0.0.0.0", + "Quincy", + "Washington", + "United States", + "Other", + "8df423be-a013-477c-88b9-8186722e3d49", + "7b1ad079-51c6-4824-91c1-9decb485ba64", + "dotnet7.0.20:otel1.8.1:ext1.3.0-beta.2-d", + "Azure", + "AppMetrics", + "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-tileemonitor/providers/microsoft.insights/components/t48da106dbe0b9268" + ] + ] + } + ] + } + }, + { + "RequestUri": "https://api.loganalytics.io/v1/workspaces/d49041ed-21aa-42c5-a6f3-6d60bb93d63c/query", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "180", + "Content-Type": "application/json", + "traceparent": "00-154cc7d705defc8173d6ade388684c0e-58f18a6b885d49f9-00", + "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.20; Microsoft Windows 10.0.22631)", + "x-ms-client-request-id": "Sanitized", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "query": "AppMetrics | where Name == 'http.server.request.duration' | where AppRoleName == 'DistroWebAppLiveTests' | top 1 by TimeGenerated", + "timespan": "PT30M" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "Retry-After,Age,WWW-Authenticate,x-resource-identities,x-ms-status-location", + "Connection": "keep-alive", + "Content-Length": "2062", + "Content-Type": "application/json; charset=utf-8", + "Date": "Fri, 07 Jun 2024 21:45:00 GMT", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Vary": "Accept-Encoding", + "Via": "1.1 draft-oms-84fccbf47-z4b84", + "X-Content-Type-Options": "nosniff" + }, + "ResponseBody": { + "tables": [ + { + "name": "PrimaryResult", + "columns": [ + { + "name": "TenantId", + "type": "string" + }, + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "Name", + "type": "string" + }, + { + "name": "ItemCount", + "type": "int" + }, + { + "name": "Sum", + "type": "real" + }, + { + "name": "Min", + "type": "real" + }, + { + "name": "Max", + "type": "real" + }, + { + "name": "Properties", + "type": "dynamic" + }, + { + "name": "OperationName", + "type": "string" + }, + { + "name": "OperationId", + "type": "string" + }, + { + "name": "ParentId", + "type": "string" + }, + { + "name": "SyntheticSource", + "type": "string" + }, + { + "name": "SessionId", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "UserAuthenticatedId", + "type": "string" + }, + { + "name": "UserAccountId", + "type": "string" + }, + { + "name": "AppVersion", + "type": "string" + }, + { + "name": "AppRoleName", + "type": "string" + }, + { + "name": "AppRoleInstance", + "type": "string" + }, + { + "name": "ClientType", + "type": "string" + }, + { + "name": "ClientModel", + "type": "string" + }, + { + "name": "ClientOS", + "type": "string" + }, + { + "name": "ClientIP", + "type": "string" + }, + { + "name": "ClientCity", + "type": "string" + }, + { + "name": "ClientStateOrProvince", + "type": "string" + }, + { + "name": "ClientCountryOrRegion", + "type": "string" + }, + { + "name": "ClientBrowser", + "type": "string" + }, + { + "name": "ResourceGUID", + "type": "string" + }, + { + "name": "IKey", + "type": "string" + }, + { + "name": "SDKVersion", + "type": "string" + }, + { + "name": "SourceSystem", + "type": "string" + }, + { + "name": "Type", + "type": "string" + }, + { + "name": "_ResourceId", + "type": "string" + } + ], + "rows": [ + [ + "d49041ed-21aa-42c5-a6f3-6d60bb93d63c", + "2024-06-07T21:44:23.4175451Z", + "http.server.request.duration", + 1, + 0.0338512, + 0.0338512, + 0.0338512, + "{\"http.request.method\":\"GET\",\"http.response.status_code\":\"200\",\"http.route\":\"/\",\"network.protocol.version\":\"1.1\",\"url.scheme\":\"http\"}", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "DistroWebAppLiveTests", + "af7c72ee-94a1-42f8-8306-395ce9147b22", + "PC", + "Other", + "Windows 10", + "0.0.0.0", + "Quincy", + "Washington", + "United States", + "Other", + "8df423be-a013-477c-88b9-8186722e3d49", + "7b1ad079-51c6-4824-91c1-9decb485ba64", + "dotnet7.0.20:otel1.8.1:ext1.3.0-beta.2-d", + "Azure", + "AppMetrics", + "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-tileemonitor/providers/microsoft.insights/components/t48da106dbe0b9268" + ] + ] + } + ] + } + }, + { + "RequestUri": "https://api.loganalytics.io/v1/workspaces/d49041ed-21aa-42c5-a6f3-6d60bb93d63c/query", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "173", + "Content-Type": "application/json", + "traceparent": "00-9759964878634526a529cfa2071f06c9-0d03ec30c641be54-00", + "User-Agent": "azsdk-net-Monitor.Query/1.1.0 (.NET 7.0.20; Microsoft Windows 10.0.22631)", + "x-ms-client-request-id": "Sanitized", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "query": "AppTraces | where Message == 'Message via ILogger' | where AppRoleName == 'DistroWebAppLiveTests' | top 1 by TimeGenerated", + "timespan": "PT30M" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "Retry-After,Age,WWW-Authenticate,x-resource-identities,x-ms-status-location", + "Connection": "keep-alive", + "Content-Length": "1989", + "Content-Type": "application/json; charset=utf-8", + "Date": "Fri, 07 Jun 2024 21:45:01 GMT", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Vary": "Accept-Encoding", + "Via": "1.1 draft-oms-84fccbf47-q7zwk", + "X-Content-Type-Options": "nosniff" + }, + "ResponseBody": { + "tables": [ + { + "name": "PrimaryResult", + "columns": [ + { + "name": "TenantId", + "type": "string" + }, + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "Message", + "type": "string" + }, + { + "name": "SeverityLevel", + "type": "int" + }, + { + "name": "Properties", + "type": "dynamic" + }, + { + "name": "Measurements", + "type": "dynamic" + }, + { + "name": "OperationName", + "type": "string" + }, + { + "name": "OperationId", + "type": "string" + }, + { + "name": "ParentId", + "type": "string" + }, + { + "name": "SyntheticSource", + "type": "string" + }, + { + "name": "SessionId", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "UserAuthenticatedId", + "type": "string" + }, + { + "name": "UserAccountId", + "type": "string" + }, + { + "name": "AppVersion", + "type": "string" + }, + { + "name": "AppRoleName", + "type": "string" + }, + { + "name": "AppRoleInstance", + "type": "string" + }, + { + "name": "ClientType", + "type": "string" + }, + { + "name": "ClientModel", + "type": "string" + }, + { + "name": "ClientOS", + "type": "string" + }, + { + "name": "ClientIP", + "type": "string" + }, + { + "name": "ClientCity", + "type": "string" + }, + { + "name": "ClientStateOrProvince", + "type": "string" + }, + { + "name": "ClientCountryOrRegion", + "type": "string" + }, + { + "name": "ClientBrowser", + "type": "string" + }, + { + "name": "ResourceGUID", + "type": "string" + }, + { + "name": "IKey", + "type": "string" + }, + { + "name": "SDKVersion", + "type": "string" + }, + { + "name": "ItemCount", + "type": "int" + }, + { + "name": "ReferencedItemId", + "type": "string" + }, + { + "name": "ReferencedType", + "type": "string" + }, + { + "name": "SourceSystem", + "type": "string" + }, + { + "name": "Type", + "type": "string" + }, + { + "name": "_ResourceId", + "type": "string" + } + ], + "rows": [ + [ + "d49041ed-21aa-42c5-a6f3-6d60bb93d63c", + "2024-06-07T21:44:23.2335203Z", + "Message via ILogger", + 1, + null, + null, + "", + "2f5dc72a16261a3a9e74e74ca0f3b4bf", + "52f6536389172c57", + "", + "", + "", + "", + "", + "", + "DistroWebAppLiveTests", + "tilee-devbox", + "PC", "Other", + "Windows 10", "0.0.0.0", - "San Jose", - "California", + "Quincy", + "Washington", "United States", "Other", - "6cbe8a21-11ea-42f8-b456-19144d0c3dde", - "375a4ad1-58c9-4f8f-bf21-7f483a813bd5", - "dotnet7.0.1:otel1.4.0:ext1.0.0-alpha.20230524.3", + "8df423be-a013-477c-88b9-8186722e3d49", + "7b1ad079-51c6-4824-91c1-9decb485ba64", + "dotnet7.0.20:otel1.8.1:ext1.3.0-beta.2-d", 1, "", "", "Azure", - "AppDependencies", - "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-mothrmonitor/providers/microsoft.insights/components/tca5ef17935bdced8" + "AppTraces", + "/subscriptions/65b2f83e-7bf1-4be3-bafc-3a4163265a52/resourcegroups/rg-tileemonitor/providers/microsoft.insights/components/t48da106dbe0b9268" ] ] } @@ -251,10 +1132,9 @@ } ], "Variables": { - "AZURE_AUTHORITY_HOST": "https://login.microsoftonline.com/", - "CONNECTION_STRING": "InstrumentationKey=375a4ad1-58c9-4f8f-bf21-7f483a813bd5;IngestionEndpoint=https://westus-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus.livediagnostics.monitor.azure.com/", + "CONNECTION_STRING": "InstrumentationKey=7b1ad079-51c6-4824-91c1-9decb485ba64;IngestionEndpoint=https://westus-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus.livediagnostics.monitor.azure.com/;ApplicationId=8df423be-a013-477c-88b9-8186722e3d49", "LOGS_ENDPOINT": "https://api.loganalytics.io", - "RandomSeed": "277228534", - "WORKSPACE_ID": "33283218-aeb0-4388-b0c0-77a9bf80a8d2" + "RandomSeed": "1564761728", + "WORKSPACE_ID": "d49041ed-21aa-42c5-a6f3-6d60bb93d63c" } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/TelemetryValidationHelper.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/TelemetryValidationHelper.cs new file mode 100644 index 000000000000..3628720bdb21 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests/TelemetryValidationHelper.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Reflection; +#if NET6_0_OR_GREATER +using System.Text.Json.Nodes; +#endif +using Azure.Monitor.Query.Models; +using NUnit.Framework; + +namespace Azure.Monitor.OpenTelemetry.AspNetCore.Integration.Tests +{ + internal static class TelemetryValidationHelper + { + public static void ValidateExpectedTelemetry(string description, LogsTable? logsTable, object expectedTelemetry) + { + TestContext.Out.WriteLine($"{nameof(ValidateExpectedTelemetry)}: '{description}'"); + + Assert.IsNotNull(logsTable, $"({description}) Expected a non-null table."); + Assert.AreEqual(1, logsTable!.Rows.Count, $"({description}) Expected 1 row in the table but found {logsTable.Rows.Count} rows."); + + var row = logsTable.Rows[0]; + foreach (PropertyInfo property in expectedTelemetry.GetType().GetProperties()) + { + if (property.Name == "Properties") + { + var jsonString = row[property.Name].ToString(); + Assert.IsNotNull(jsonString, $"({description}) Expected a non-null value for {property.Name}"); + var expectedProperties = property.GetValue(expectedTelemetry, null) as List>; + Assert.IsNotNull(expectedProperties, $"({description}) Expected a non-null value for {nameof(expectedTelemetry)}.Properties"); + + ValidateProperties( + description: description, + jsonString: jsonString!, + expectedProperties: expectedProperties!); + } + else + { + TestContext.Out.WriteLine($"Property: '{property.Name}' ExpectedValue: '{property.GetValue(expectedTelemetry, null)}' ActualValue: '{row[property.Name]}'"); + + Assert.AreEqual( + expected: property.GetValue(expectedTelemetry, null), + actual: row[property.Name], + message: $"({description}) Expected {property.Name} to be '{property.GetValue(expectedTelemetry, null)}' but found '{logsTable.Rows[0][property.Name]}'."); + } + } + + TestContext.Out.WriteLine(); + } + + private static void ValidateProperties(string description, string jsonString, List> expectedProperties) + { +#if NET6_0_OR_GREATER + var jsonNode = JsonNode.Parse(jsonString); + Assert.IsNotNull(jsonNode, $"({description}) Expected a non-null JSON node."); + + foreach (var expectedProperty in expectedProperties) + { + var jsonValue = jsonNode![expectedProperty.Key]; + Assert.IsNotNull(jsonValue, $"({description}) Expected a non-null JSON value for Properties.'{expectedProperty.Key}'."); + var actualValue = jsonValue!.ToString(); + + TestContext.Out.WriteLine($"Properties.'{expectedProperty.Key}' ExpectedValue: '{expectedProperty.Value}' ActualValue: '{actualValue}'"); + + Assert.AreEqual( + expected: expectedProperty.Value, + actual: actualValue, + message: $"({description}) Expected Properties.'{expectedProperty.Key}' to be '{expectedProperty.Value}' but found '{actualValue}'."); + } +#endif + } + + public struct ExpectedAppDependency + { + public string Data { get; set; } + public string AppRoleName { get; set; } + + // TODO: ADD REMAINING PROPERTIES IN FOLLOW UP PR. + } + + public struct ExpectedAppRequest + { + public string Url { get; set; } + public string AppRoleName { get; set; } + + // TODO: ADD REMAINING PROPERTIES IN FOLLOW UP PR. + } + + public struct ExpectedAppMetric + { + public string Name { get; set; } + public string AppRoleName { get; set; } + public List> Properties { get; set; } + + // TODO: ADD REMAINING PROPERTIES IN FOLLOW UP PR. + } + + public struct ExpectedAppTrace + { + public string Message { get; set; } + public string AppRoleName { get; set; } + + // TODO: ADD REMAINING PROPERTIES IN FOLLOW UP PR. + } + } +}