diff --git a/build/ci-build.yml b/build/ci-build.yml index ac0c22b5..234a03ee 100644 --- a/build/ci-build.yml +++ b/build/ci-build.yml @@ -18,6 +18,10 @@ parameters: - name: 'Package.Version.ManualTrigger' type: string default: 'preview' + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' resources: repositories: @@ -27,7 +31,6 @@ resources: endpoint: arcus-azure variables: - - group: 'Arcus Observability - Integration Testing' - group: 'Arcus - GitHub Package Registry' - group: 'Build Configuration' - template: ./variables/build.yml @@ -75,78 +78,27 @@ stages: dependsOn: Build condition: succeeded() jobs: - - job: UnitTests - displayName: 'Run unit tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: test/run-unit-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Unit' + - template: templates/run-unit-tests.yml - stage: IntegrationTests displayName: Integration Tests dependsOn: Build condition: succeeded() jobs: - - job: IntegrationTests - displayName: 'Run integration tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: test/run-integration-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Integration' - category: 'Integration' + - template: templates/run-self-contained-integration-tests.yml + parameters: + azureServiceConnection: '${{ parameters.azureServiceConnection }}' - stage: DockerTests displayName: Docker Tests dependsOn: Build condition: succeeded() jobs: - - job: DockerTests - displayName: 'Run Docker tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: templates/run-docker-integration-tests.yml - parameters: - dockerProjectName: '$(Project).Tests.Runtimes.AzureFunction' - httpPort: '$(AzureFunctions.HttpPort)' + - template: templates/run-docker-integration-tests.yml + parameters: + dockerProjectName: '$(Project).Tests.Runtimes.AzureFunction' + httpPort: '$(AzureFunctions.HttpPort)' + azureServiceConnection: '${{ parameters.azureServiceConnection }}' - stage: ReleaseToMyget displayName: 'Release to MyGet' diff --git a/build/deploy-test-resources.yml b/build/deploy-test-resources.yml new file mode 100644 index 00000000..479654b5 --- /dev/null +++ b/build/deploy-test-resources.yml @@ -0,0 +1,45 @@ +name: Arcus Observability - Deploy test resources + +trigger: none +pr: none + +parameters: + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' + - name: resourceGroupName + displayName: 'Resource group name' + default: arcus-observability-dev-we-rg + +variables: + - template: ./variables/build.yml + - template: ./variables/test.yml + +stages: + - stage: Deploy + jobs: + - job: DeployBicep + displayName: 'Deploy test resources' + pool: + vmImage: '$(Vm.Image)' + steps: + - task: AzureCLI@2 + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + azureSubscription: '${{ parameters.azureServiceConnection }}' + addSpnToEnvironment: true + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + $objectId = (az ad sp show --id $env:servicePrincipalId | ConvertFrom-Json).id + az deployment sub create ` + --location westeurope ` + --template-file ./build/templates/deploy-test-resources.bicep ` + --parameters location=westeurope ` + --parameters resourceGroupName=${{ parameters.resourceGroupName }} ` + --parameters instrumentationKey_secretName=${{ variables['Arcus.Observability.ApplicationInsights.InstrumentationKey.SecretName'] }} ` + --parameters workspaceId_secretName=${{ variables['Arcus.Observability.LogAnalytics.WorkspaceId.SecretName'] }} ` + --parameters keyVaultName=${{ variables['Arcus.Observability.KeyVault.Name'] }} ` + --parameters servicePrincipal_objectId=$objectId \ No newline at end of file diff --git a/build/nuget-release.yml b/build/nuget-release.yml index d81119d3..6a14f33a 100644 --- a/build/nuget-release.yml +++ b/build/nuget-release.yml @@ -6,6 +6,10 @@ pr: none parameters: - name: 'Package.Version' type: 'string' + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' resources: repositories: @@ -15,7 +19,6 @@ resources: endpoint: arcus-azure variables: - - group: 'Arcus Observability - Integration Testing' - group: 'Build Configuration' - template: ./variables/build.yml - template: ./variables/test.yml @@ -57,78 +60,27 @@ stages: dependsOn: Build condition: succeeded() jobs: - - job: UnitTests - displayName: 'Run unit tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: test/run-unit-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Unit' + - template: templates/run-unit-tests.yml - stage: IntegrationTests displayName: Integration Tests dependsOn: Build condition: succeeded() jobs: - - job: IntegrationTests - displayName: 'Run integration tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: test/run-integration-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Integration' - category: 'Integration' + - template: templates/run-self-contained-integration-tests.yml + parameters: + azureServiceConnection: '${{ parameters.azureServiceConnection }}' - stage: DockerTests displayName: Docker Tests dependsOn: Build condition: succeeded() jobs: - - job: DockerTests - displayName: 'Run Docker tests' - pool: - vmImage: '$(Vm.Image)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: templates/run-docker-integration-tests.yml - parameters: - dockerProjectName: '$(Project).Tests.Runtimes.AzureFunction' - httpPort: '$(AzureFunctions.HttpPort)' + - template: templates/run-docker-integration-tests.yml + parameters: + dockerProjectName: '$(Project).Tests.Runtimes.AzureFunction' + httpPort: '$(AzureFunctions.HttpPort)' + azureServiceConnection: '${{ parameters.azureServiceConnection }}' - stage: Release displayName: 'Release to NuGet.org' diff --git a/build/templates/deploy-test-resources.bicep b/build/templates/deploy-test-resources.bicep new file mode 100644 index 00000000..72e1e9ab --- /dev/null +++ b/build/templates/deploy-test-resources.bicep @@ -0,0 +1,90 @@ +// Define the location for the deployment of the components. +param location string + +// Define the name of the resource group where the components will be deployed. +param resourceGroupName string + +// Define the name of the secret that will store the Application Insights Instrumentation Key. +param instrumentationKey_secretName string + +// Define the name of the secret that will store the Application Insights workspace resource ID. +param workspaceId_secretName string + +// Define the name of the Key Vault. +param keyVaultName string + +// Define the Service Principal ID that needs access full access to the deployed resource group. +param servicePrincipal_objectId string + +targetScope='subscription' + +module resourceGroup 'br/public:avm/res/resources/resource-group:0.2.3' = { + name: 'resourceGroupDeployment' + params: { + name: resourceGroupName + location: location + } +} + +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' existing = { + name: resourceGroupName +} + +module workspace 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'workspaceDeployment' + dependsOn: [ + resourceGroup + ] + scope: rg + params: { + name: 'arcus-observability-dev-we-workspace' + location: location + } +} + +module component 'br/public:avm/res/insights/component:0.3.0' = { + name: 'componentDeployment' + dependsOn: [ + resourceGroup + ] + scope: rg + params: { + name: 'arcus-observability-dev-we-app-insights' + workspaceResourceId: workspace.outputs.resourceId + location: location + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: '73c42c96-874c-492b-b04d-ab87d138a893' + } + ] + } +} + +module vault 'br/public:avm/res/key-vault/vault:0.6.1' = { + name: 'vaultDeployment' + dependsOn: [ + resourceGroup + ] + scope: rg + params: { + name: keyVaultName + location: location + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: 'Key Vault Secrets officer' + } + ] + secrets: [ + { + name: instrumentationKey_secretName + value: component.outputs.instrumentationKey + } + { + name: workspaceId_secretName + value: workspace.outputs.logAnalyticsWorkspaceId + } + ] + } +} diff --git a/build/templates/import-keyvault-secrets.yml b/build/templates/import-keyvault-secrets.yml new file mode 100644 index 00000000..dd4ba707 --- /dev/null +++ b/build/templates/import-keyvault-secrets.yml @@ -0,0 +1,31 @@ +parameters: + azureServiceConnection: '' + +steps: + - task: AzureCLI@2 + displayName: 'Import secrets from Azure Key Vault' + inputs: + azureSubscription: '${{ parameters.azureServiceConnection }}' + addSpnToEnvironment: true + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name Arcus.Scripting.DevOps -AllowClobber + + Set-AzDevOpsVariable -Name 'Arcus.Observability.TenantId' -Value $env:tenantId + Set-AzDevOpsVariable -Name 'Arcus.Observability.ServicePrincipal.ClientId' -Value $env:servicePrincipalId + Set-AzDevOpsVariable -Name 'Arcus.Observability.ServicePrincipal.ClientSecret' -Value $env:servicePrincipalKey + + $keyVaultName = $env:ARCUS_OBSERVABILITY_KEYVAULT_NAME + Write-Host "Importing secrets from Key Vault: $keyVaultName" + + $instrumentationKey_secretName = $env:ARCUS_OBSERVABILITY_APPLICATIONINSIGHTS_INSTRUMENTATIONKEY_SECRETNAME + Write-Host "Importing secret: $instrumentationKey_secretName" + $instrumentationKeySecret = az keyvault secret show --name "$instrumentationKey_secretName" --vault-name "$keyVaultName" | ConvertFrom-Json + Set-AzDevOpsVariable -AsSecret -Name 'Arcus.Observability.ApplicationInsights.InstrumentationKey' -Value $instrumentationKeySecret.value + + $resourceId_secretName = $env:ARCUS_OBSERVABILITY_LOGANALYTICS_WORKSPACEID_SECRETNAME + Write-Host "Importing secret: $resourceId_secretName" + $resourceIdSecret = az keyvault secret show --name "$resourceId_secretName" --vault-name "$keyVaultName" | ConvertFrom-Json + Set-AzDevOpsVariable -AsSecret -Name 'Arcus.Observability.ApplicationInsights.LogAnalytics.WorkspaceId' -Value $resourceIdSecret.value \ No newline at end of file diff --git a/build/templates/run-docker-integration-tests.yml b/build/templates/run-docker-integration-tests.yml index 97e817ad..174bb0f2 100644 --- a/build/templates/run-docker-integration-tests.yml +++ b/build/templates/run-docker-integration-tests.yml @@ -1,54 +1,82 @@ parameters: dockerProjectName: '' httpPort: '' + azureServiceConnection: '' -steps: - - bash: | - if [ -z "$PROJECT_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"dockerProjectName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$HTTP_PORT" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"httpPort\"" - echo "##vso[task.complete result=Failed;]" - fi - env: - PROJECT_NAME: ${{ parameters.dockerProjectName }} - HTTP_PORT: ${{ parameters.httpPort }} - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.Version))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - - task: Docker@1 - displayName: 'Build Docker image from ${{ parameters.dockerProjectName }}' - inputs: - dockerFile: src/${{ parameters.dockerProjectName }}/Dockerfile - imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)' - useDefaultContext: false - buildContext: src - - task: Docker@1 - displayName: 'Run new project Docker image from ${{ parameters.dockerProjectName }}' - inputs: - command: 'Run an image' - imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)' - containerName: '${{ parameters.dockerProjectName }}' - ports: '${{ parameters.httpPort }}:80' - envVars: | - APPINSIGHTS_INSTRUMENTATIONKEY=$(ApplicationInsights.InstrumentationKey) - AzureWebJobsStorage=$(Arcus.AzureFunctions.AzureWebJobsStorage) - - template: test/run-integration-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Integration' - category: 'Docker' - - task: Bash@3 - inputs: - targetType: 'inline' - script: | - docker logs ${{ parameters.dockerProjectName }} - failOnStderr: true - displayName: Show ${{ parameters.dockerProjectName }} logs - condition: always() +jobs: + - job: DockerTests + displayName: 'Run Docker tests' + pool: + vmImage: '$(Vm.Image)' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download build artifacts' + inputs: + artifact: 'Build' + path: '$(Build.SourcesDirectory)' + + - bash: | + if [ -z "$PROJECT_NAME" ]; then + echo "##vso[task.logissue type=error;]Missing template parameter \"dockerProjectName\"" + echo "##vso[task.complete result=Failed;]" + fi + if [ -z "$HTTP_PORT" ]; then + echo "##vso[task.logissue type=error;]Missing template parameter \"httpPort\"" + echo "##vso[task.complete result=Failed;]" + fi + env: + PROJECT_NAME: ${{ parameters.dockerProjectName }} + HTTP_PORT: ${{ parameters.httpPort }} + + - task: UseDotNet@2 + displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' + inputs: + packageType: 'sdk' + version: '$(DotNet.Sdk.PreviousVersion)' + + - task: UseDotNet@2 + displayName: 'Import .NET Core SDK ($(DotNet.Sdk.Version))' + inputs: + packageType: 'sdk' + version: '$(DotNet.Sdk.Version)' + includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) + + - template: import-keyvault-secrets.yml + parameters: + azureServiceConnection: '${{ parameters.azureServiceConnection }}' + + - task: Docker@1 + displayName: 'Build Docker image from ${{ parameters.dockerProjectName }}' + inputs: + dockerFile: src/${{ parameters.dockerProjectName }}/Dockerfile + imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)' + useDefaultContext: false + buildContext: src + + - task: Docker@1 + displayName: 'Run new project Docker image from ${{ parameters.dockerProjectName }}' + inputs: + command: 'Run an image' + imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)' + containerName: '${{ parameters.dockerProjectName }}' + ports: '${{ parameters.httpPort }}:80' + envVars: | + APPINSIGHTS_INSTRUMENTATIONKEY=$(Arcus.Observability.ApplicationInsights.InstrumentationKey) + AzureWebJobsStorage=$(Arcus.AzureFunctions.AzureWebJobsStorage) + + - template: test/run-integration-tests.yml@templates + parameters: + dotnetSdkVersion: '$(DotNet.Sdk.Version)' + includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) + projectName: '$(Project).Tests.Integration' + category: 'Docker' + + - task: Bash@3 + inputs: + targetType: 'inline' + script: | + docker logs ${{ parameters.dockerProjectName }} + failOnStderr: true + displayName: Show ${{ parameters.dockerProjectName }} logs + condition: always() + \ No newline at end of file diff --git a/build/templates/run-self-contained-integration-tests.yml b/build/templates/run-self-contained-integration-tests.yml new file mode 100644 index 00000000..6cf3e5d3 --- /dev/null +++ b/build/templates/run-self-contained-integration-tests.yml @@ -0,0 +1,31 @@ +parameters: + azureServiceConnection: '' + +jobs: + - job: IntegrationTests + displayName: 'Run integration tests' + pool: + vmImage: '$(Vm.Image)' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download build artifacts' + inputs: + artifact: 'Build' + path: '$(Build.SourcesDirectory)' + + - task: UseDotNet@2 + displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' + inputs: + packageType: 'sdk' + version: '$(DotNet.Sdk.PreviousVersion)' + + - template: import-keyvault-secrets.yml + parameters: + azureServiceConnection: '${{ parameters.azureServiceConnection }}' + + - template: test/run-integration-tests.yml@templates + parameters: + dotnetSdkVersion: '$(DotNet.Sdk.Version)' + includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) + projectName: '$(Project).Tests.Integration' + category: 'Integration' \ No newline at end of file diff --git a/build/templates/run-unit-tests.yml b/build/templates/run-unit-tests.yml new file mode 100644 index 00000000..8f3a4dbd --- /dev/null +++ b/build/templates/run-unit-tests.yml @@ -0,0 +1,23 @@ +jobs: + - job: UnitTests + displayName: 'Run unit tests' + pool: + vmImage: '$(Vm.Image)' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download build artifacts' + inputs: + artifact: 'Build' + path: '$(Build.SourcesDirectory)' + + - task: UseDotNet@2 + displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' + inputs: + packageType: 'sdk' + version: '$(DotNet.Sdk.PreviousVersion)' + + - template: test/run-unit-tests.yml@templates + parameters: + dotnetSdkVersion: '$(DotNet.Sdk.Version)' + includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) + projectName: '$(Project).Tests.Unit' \ No newline at end of file diff --git a/build/variables/test.yml b/build/variables/test.yml index f6ec8be2..a375fdf5 100644 --- a/build/variables/test.yml +++ b/build/variables/test.yml @@ -1,2 +1,5 @@ variables: - AzureFunctions.HttpPort: 5000 \ No newline at end of file + AzureFunctions.HttpPort: 5000 + Arcus.Observability.KeyVault.Name: 'arcus-observability-kv' + Arcus.Observability.ApplicationInsights.InstrumentationKey.SecretName: 'ApplicationInsights-InstrumentationKey' + Arcus.Observability.LogAnalytics.WorkspaceId.SecretName: 'LogAnalytics-WorkspaceId' \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/Arcus.Observability.Tests.Integration.csproj b/src/Arcus.Observability.Tests.Integration/Arcus.Observability.Tests.Integration.csproj index 94e8bc08..c910fe98 100644 --- a/src/Arcus.Observability.Tests.Integration/Arcus.Observability.Tests.Integration.csproj +++ b/src/Arcus.Observability.Tests.Integration/Arcus.Observability.Tests.Integration.csproj @@ -10,13 +10,14 @@ - + + + + - - diff --git a/src/Arcus.Observability.Tests.Integration/AssertX.cs b/src/Arcus.Observability.Tests.Integration/AssertX.cs index c44ae3f1..ada57e27 100644 --- a/src/Arcus.Observability.Tests.Integration/AssertX.cs +++ b/src/Arcus.Observability.Tests.Integration/AssertX.cs @@ -25,7 +25,9 @@ public static void Any(IEnumerable collection, Action assertion) { Guard.NotNull(collection, nameof(collection), "Requires collection of elements to find a single element that matches the assertion"); Guard.NotNull(assertion, nameof(assertion), "Requires an element assertion to verify if an single element in the collection matches"); - + + Assert.NotEmpty(collection); + T[] array = collection.ToArray(); var stack = new Stack>(); diff --git a/src/Arcus.Observability.Tests.Integration/Configuration/ServicePrincipal.cs b/src/Arcus.Observability.Tests.Integration/Configuration/ServicePrincipal.cs new file mode 100644 index 00000000..1ebd8834 --- /dev/null +++ b/src/Arcus.Observability.Tests.Integration/Configuration/ServicePrincipal.cs @@ -0,0 +1,57 @@ +using Arcus.Testing; +using GuardNet; + +namespace Arcus.Observability.Tests.Integration.Configuration +{ + /// + /// Represents the service principal to authenticate against Azure services. + /// + public class ServicePrincipal + { + /// + /// Initializes a new instance of the class. + /// + public ServicePrincipal(string tenantId, string clientId, string clientSecret) + { + Guard.NotNullOrWhitespace(tenantId, nameof(tenantId)); + Guard.NotNullOrWhitespace(clientId, nameof(clientId)); + Guard.NotNullOrWhitespace(clientSecret, nameof(clientSecret)); + + TenantId = tenantId; + ClientId = clientId; + ClientSecret = clientSecret; + } + + /// + /// Gets the tenant ID of the Azure Active Directory tenant. + /// + public string TenantId { get; } + + /// + /// Gets the client ID of the service principal. + /// + public string ClientId { get; } + + /// + /// Gets the client secret of the service principal. + /// + public string ClientSecret { get; } + } + + /// + /// Extensions on the to load the service principal from the test configuration. + /// + public static class TestConfigExtensions + { + /// + /// Loads the service principal from the test configuration. + /// + public static ServicePrincipal GetServicePrincipal(this TestConfig config) + { + return new ServicePrincipal( + config["Arcus:TenantId"], + config["Arcus:ServicePrincipal:ClientId"], + config["Arcus:ServicePrincipal:ClientSecret"]); + } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/IntegrationTest.cs b/src/Arcus.Observability.Tests.Integration/IntegrationTest.cs index 4d244eec..dd0c3898 100644 --- a/src/Arcus.Observability.Tests.Integration/IntegrationTest.cs +++ b/src/Arcus.Observability.Tests.Integration/IntegrationTest.cs @@ -1,21 +1,15 @@ -using Arcus.Testing.Logging; -using Microsoft.Extensions.Configuration; +using Arcus.Testing; using Xunit.Abstractions; namespace Arcus.Observability.Tests.Integration { public class IntegrationTest { - protected IConfiguration Configuration { get; } + protected TestConfig Configuration { get; } public IntegrationTest(ITestOutputHelper testOutput) { - // The appsettings.local.json allows users to override (gitignored) settings locally for testing purposes - Configuration = new ConfigurationBuilder() - .AddJsonFile(path: "appsettings.json") - .AddJsonFile(path: "appsettings.local.json", optional: true) - .AddEnvironmentVariables() - .Build(); + Configuration = TestConfig.Create(); } } } \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AppInsightsClient.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AppInsightsClient.cs new file mode 100644 index 00000000..706a65db --- /dev/null +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AppInsightsClient.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Arcus.Observability.Tests.Integration.Configuration; +using Arcus.Testing; +using Azure.Identity; +using Azure.Monitor.Query; +using Azure.Monitor.Query.Models; +using Microsoft.Azure.ApplicationInsights.Query.Models; +using Newtonsoft.Json; + +namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights +{ + /// + /// Represents a remote client to query telemetry data from the Azure Application Insights instance. + /// + public class AppInsightsClient : ITelemetryQueryClient + { + private readonly LogsQueryClient _queryClient; + private readonly QueryTimeRange _timeRange; + private readonly string _workspaceId; + + /// + /// Initializes a new instance of the class. + /// + public AppInsightsClient(TestConfig config) + { + ServicePrincipal servicePrincipal = config.GetServicePrincipal(); + _queryClient = new LogsQueryClient(new ClientSecretCredential(servicePrincipal.TenantId, servicePrincipal.ClientId, servicePrincipal.ClientSecret)); + _workspaceId = config["Arcus:ApplicationInsights:LogAnalytics:WorkspaceId"]; + _timeRange = new QueryTimeRange(TimeSpan.FromDays(1)); + } + + /// + /// Gets the tracked traces from the Azure Application Insights instance. + /// + public async Task GetTracesAsync() + { + IReadOnlyCollection rows = + await QueryLogsAsync("AppTraces | project Message, OperationId, ParentId, AppRoleName, Properties"); + + return rows.Select(row => + { + string message = row[0].ToString(); + string operationId = row[1].ToString(); + string parentId = row[2].ToString(); + var operation = new OperationResult(operationId, parentId); + + string roleName = row[3].ToString(); + string customDimensionsTxt = row[4].ToString(); + var customDimensions = JsonConvert.DeserializeObject>(customDimensionsTxt); + + return new EventsTraceResult(message, roleName, operation, customDimensions); + + }).ToArray(); + } + + /// + /// Gets the tracked metrics from the Azure Application Insights instance. + /// + public Task GetMetricsAsync(string metricName) + { + throw new NotImplementedException(); + } + + /// + /// Gets the tracked custom events from the Azure Application Insights instance. + /// + public async Task GetCustomEventsAsync() + { + IReadOnlyCollection rows = + await QueryLogsAsync("AppEvents | project Name, AppRoleName, Properties"); + + return rows.Select(row => + { + string name = row[0].ToString(); + string roleName = row[1].ToString(); + + var customDimensionsTxt = row[2].ToString(); + var customDimensions = JsonConvert.DeserializeObject>(customDimensionsTxt); + + return new EventsCustomEventResult(name, roleName, customDimensions); + + }).ToArray(); + } + + /// + /// Gets the tracked requests from the Azure Application Insights instance. + /// + public async Task GetRequestsAsync() + { + IReadOnlyCollection rows = + await QueryLogsAsync( + "AppRequests | project Id, Name, Source, Url, Success, ResultCode, AppRoleName, OperationId, ParentId, Properties"); + + return rows.Select(row => + { + string id = row[0].ToString(); + string name = row[1].ToString(); + string source = row[2].ToString(); + string url = row[3].ToString(); + bool success = bool.Parse(row[4].ToString() ?? string.Empty); + string resultCode = row[5].ToString(); + string roleName = row[6].ToString(); + + string operationId = row[7].ToString(); + string operationParentId = row[8].ToString(); + var operation = new OperationResult(operationId, operationParentId, name); + + var customDimensionsTxt = row[9].ToString(); + var customDimensions = JsonConvert.DeserializeObject>(customDimensionsTxt); + + return new EventsRequestResult(id, name, source, url, success, resultCode, roleName, operation, customDimensions); + + }).ToArray(); + } + + /// + /// Gets the tracked dependencies from the Azure Application Insights instance. + /// + public async Task GetDependenciesAsync() + { + IReadOnlyCollection rows = + await QueryLogsAsync("AppDependencies | project Id, Target, DependencyType, Name, Data, Success, ResultCode, AppRoleName, OperationId, ParentId, Properties"); + + return rows.Select(row => + { + string id = row[0].ToString(); + string target = row[1].ToString(); + string type = row[2].ToString(); + string name = row[3].ToString(); + string data = row[4].ToString(); + + bool success = bool.Parse(row[5].ToString() ?? string.Empty); + var resultCodeTxt = row[6].ToString(); + int resultCode = string.IsNullOrWhiteSpace(resultCodeTxt) ? 0 : int.Parse(resultCodeTxt); + + string roleName = row[7].ToString(); + + string operationId = row[8].ToString(); + string operationParentId = row[9].ToString(); + var operation = new OperationResult(operationId, operationParentId); + + var customDimensionsTxt = row[10].ToString(); + var customDimensions = JsonConvert.DeserializeObject>(customDimensionsTxt); + + return new EventsDependencyResult(id, type, target, data, success, resultCode, name, roleName, operation, customDimensions); + + }).ToArray(); + } + + /// + /// Gets the tracked exceptions from the Azure Application Insights instance. + /// + public async Task GetExceptionsAsync() + { + IReadOnlyCollection rows = + await QueryLogsAsync("AppExceptions | project OuterMessage, OperationId, ParentId, AppRoleName, Properties"); + + return rows.Select(row => + { + string message = row[0].ToString(); + string operationId = row[1].ToString(); + string parentId = row[2].ToString(); + var operation = new OperationResult(operationId, parentId); + + string roleName = row[3].ToString(); + + string customDimensionsTxt = row[4].ToString(); + var customDimensions = JsonConvert.DeserializeObject>(customDimensionsTxt); + + return new EventsExceptionResult(message, operation, roleName, customDimensions); + + }).ToArray(); + } + + private async Task> QueryLogsAsync(string query) + { + LogsQueryResult response = await _queryClient.QueryWorkspaceAsync( + _workspaceId, + query, + timeRange: _timeRange, + new LogsQueryOptions { ServerTimeout = TimeSpan.FromSeconds(3) }); + + return response.Table.Rows; + } + } +} diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsClient.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsClient.cs deleted file mode 100644 index 00e1df10..00000000 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsClient.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using GuardNet; -using Microsoft.Azure.ApplicationInsights.Query; -using Microsoft.Azure.ApplicationInsights.Query.Models; -using Xunit; -using Xunit.Sdk; - -namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights -{ - /// - /// Represents a simple client to interact with the available features of Application Insights. - /// - public class ApplicationInsightsClient - { - private const string PastHalfHourTimeSpan = "PT30M"; - - private readonly IApplicationInsightsDataClient _dataClient; - private readonly string _applicationId; - - /// - /// Initializes a new instance of the class. - /// - /// The core client to retrieve telemetry from Application Insights. - /// The application ID to identify the Application Insights resource to request telemetry from. - /// Thrown when the is null. - /// Thrown when the is blank. - public ApplicationInsightsClient(IApplicationInsightsDataClient dataClient, string applicationId) - { - Guard.NotNull(dataClient, nameof(dataClient), "Requires a data client implementation to retrieve telemetry form Application Insights"); - Guard.NotNullOrWhitespace(applicationId, nameof(applicationId), "Requires an application ID to identify the Application Insights resource to request telemetry from"); - - _dataClient = dataClient; - _applicationId = applicationId; - } - - /// - /// Gets the currently available dependencies tracked on Application Insights. - /// - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetDependenciesAsync() - { - EventsResults result = - await _dataClient.Events.GetDependencyEventsAsync(_applicationId, timespan: PastHalfHourTimeSpan); - - Assert.NotEmpty(result.Value); - return result.Value.ToArray(); - } - - /// - /// Gets the currently available requests tracked on Application Insights. - /// - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetRequestsAsync() - { - EventsResults result = - await _dataClient.Events.GetRequestEventsAsync(_applicationId, timespan: PastHalfHourTimeSpan); - - Assert.NotEmpty(result.Value); - return result.Value.ToArray(); - } - - /// - /// Gets the currently available metrics tracked on Application Insights. - /// - /// The schema body content of the request to filter out metrics. - /// Thrown when the is null. - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetMetricsAsync(MetricsPostBodySchema body) - { - Guard.NotNull(body, nameof(body), "Requires a metrics body schema to request specific metrics from Application Insights"); - - IList items = - await _dataClient.Metrics.GetMultipleAsync(_applicationId, new List { body }); - - return items.ToArray(); - } - - /// - /// Gets the currently available exceptions tracked on Application Insights. - /// - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetExceptionsAsync() - { - EventsResults result = - await _dataClient.Events.GetExceptionEventsAsync(_applicationId, timespan: PastHalfHourTimeSpan); - - Assert.NotEmpty(result.Value); - return result.Value.ToArray(); - } - - /// - /// Gets the currently available traces tracked on Application Insights. - /// - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetTracesAsync() - { - EventsResults result = - await _dataClient.Events.GetTraceEventsAsync(_applicationId, timespan: PastHalfHourTimeSpan); - - Assert.NotEmpty(result.Value); - return result.Value.ToArray(); - } - - /// - /// Gets the currently available events tracked on Application Insights. - /// - /// Thrown when the retrieved telemetry doesn't contain any items. - public async Task GetEventsAsync() - { - EventsResults result = - await _dataClient.Events.GetCustomEventsAsync(_applicationId, timespan: PastHalfHourTimeSpan); - - Assert.NotEmpty(result.Value); - return result.Value.ToArray(); - } - } -} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkExtensionTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkExtensionTests.cs index adcd9f27..0008d550 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkExtensionTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkExtensionTests.cs @@ -1,5 +1,4 @@ using System; -using System.Net.Http; using System.Threading.Tasks; using Arcus.Observability.Correlation; using Arcus.Observability.Telemetry.Core; diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkTests.cs index 18c83ce3..202d72cc 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ApplicationInsightsSinkTests.cs @@ -3,50 +3,56 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Arcus.Observability.Telemetry.Serilog.Sinks.ApplicationInsights.Configuration; -using Arcus.Observability.Telemetry.Serilog.Sinks.ApplicationInsights.Converters; -using Arcus.Observability.Tests.Core; +using Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights.Fixture; +using Arcus.Testing; using Bogus; using GuardNet; -using Microsoft.Azure.ApplicationInsights.Query; +using Microsoft.Azure.ApplicationInsights.Query.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Polly; -using Polly.Timeout; using Serilog; +using Serilog.Configuration; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; using ILogger = Microsoft.Extensions.Logging.ILogger; +using InMemoryLogSink = Arcus.Observability.Tests.Core.InMemoryLogSink; namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights { + public enum TestLocation { Local, Remote } + [Trait(name: "Category", value: "Integration")] public class ApplicationInsightsSinkTests : IntegrationTest { + private readonly ITestOutputHelper _testOutput; private readonly InMemoryLogSink _memoryLogSink; + private readonly InMemoryApplicationInsightsTelemetryConverter _telemetrySink; + /// /// Gets the test generator to create bogus content during the integration test. /// - protected static readonly Faker BogusGenerator = new Faker(); + protected static readonly Faker BogusGenerator = new(); /// /// Initializes a new instance of the class. /// - public ApplicationInsightsSinkTests(ITestOutputHelper outputWriter) : base(outputWriter) + protected ApplicationInsightsSinkTests(ITestOutputHelper outputWriter) : base(outputWriter) { _memoryLogSink = new InMemoryLogSink(); - TestOutput = outputWriter; + _testOutput = outputWriter; ApplicationInsightsSinkOptions = new ApplicationInsightsSinkOptions(); LoggerConfiguration = new LoggerConfiguration(); - InstrumentationKey = Configuration.GetValue("ApplicationInsights:InstrumentationKey"); - ApplicationId = Configuration.GetValue("ApplicationInsights:ApplicationId"); + InstrumentationKey = Configuration.GetValue("Arcus:ApplicationInsights:InstrumentationKey"); + _telemetrySink = new InMemoryApplicationInsightsTelemetryConverter(); } /// - /// Gets the to write information to the test output. + /// Gets or sets the test location to determine where the telemetry should be logged to. /// - protected ITestOutputHelper TestOutput { get; } + protected TestLocation TestLocation { get; set; } /// /// Gets the instrumentation key to connect to the Azure Application Insights instance. @@ -75,12 +81,35 @@ protected ILogger Logger { get { + ApplicationInsightsSinkOptions userOptions = ApplicationInsightsSinkOptions; + _telemetrySink.Options = userOptions; + LoggerConfiguration .MinimumLevel.Verbose() - .WriteTo.Sink(new XunitLogEventSink(TestOutput)) - .WriteTo.ApplicationInsights("InstrumentationKey=" + InstrumentationKey, ApplicationInsightsTelemetryConverter.Create(ApplicationInsightsSinkOptions)) + .WriteTo.Sink(new XunitLogEventSink(_testOutput)) .WriteTo.Sink(_memoryLogSink); + switch (TestLocation) + { + case TestLocation.Local: + LoggerConfiguration.WriteTo.ApplicationInsights(_telemetrySink); + break; + + case TestLocation.Remote: + LoggerConfiguration.WriteTo.AzureApplicationInsightsWithConnectionString("InstrumentationKey=" + InstrumentationKey, options => + { + options.Correlation.OperationIdPropertyName = userOptions.Correlation.OperationIdPropertyName; + options.Correlation.OperationParentIdPropertyName = userOptions.Correlation.OperationParentIdPropertyName; + options.Correlation.TransactionIdPropertyName = userOptions.Correlation.TransactionIdPropertyName; + + options.Exception.IncludeProperties = userOptions.Exception.IncludeProperties; + options.Exception.PropertyFormat = userOptions.Exception.PropertyFormat; + + options.Request.GenerateId = userOptions.Request.GenerateId; + }); + break; + } + ILogger logger = CreateLogger(LoggerConfiguration); return logger; } @@ -100,6 +129,9 @@ protected ILogger CreateLogger(LoggerConfiguration config) { Guard.NotNull(config, nameof(config), "Requires a Serilog logger configuration instance to setup the test logger used during the test"); + _telemetrySink.Options = ApplicationInsightsSinkOptions; + config.WriteTo.ApplicationInsights(_telemetrySink); + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(config.CreateLogger(), dispose: true)); ILogger logger = loggerFactory.CreateLogger(); @@ -113,7 +145,7 @@ protected ILogger CreateLogger(LoggerConfiguration config) protected Dictionary CreateTestTelemetryContext([CallerMemberName] string memberName = "") { var testId = Guid.NewGuid(); - TestOutput.WriteLine("Testing '{0}' using {1}", memberName, testId); + _testOutput.WriteLine("Testing '{0}' using {1}", memberName, testId); return new Dictionary { @@ -127,65 +159,35 @@ protected Dictionary CreateTestTelemetryContext([CallerMemberNam /// /// The assertion function that takes in an Application Insights client which provides access to the available telemetry. /// Thrown when the is null. - /// Thrown when the failed to be verified within the configured timeout. - protected async Task RetryAssertUntilTelemetryShouldBeAvailableAsync(Func assertion) + /// Thrown when the failed to be verified within the configured timeout. + protected async Task RetryAssertUntilTelemetryShouldBeAvailableAsync(Func assertion) { - Guard.NotNull(assertion, nameof(assertion), "Requires an assertion function to correctly verify if the logged telemetry is tracked in Application Insights"); + Guard.NotNull(assertion, nameof(assertion)); - using (ApplicationInsightsDataClient dataClient = CreateApplicationInsightsClient()) + if (TestLocation is TestLocation.Remote) { - await RetryAssertUntilTelemetryShouldBeAvailableAsync(async () => - { - var client = new ApplicationInsightsClient(dataClient, ApplicationId); - await assertion(client); - }, timeout: TimeSpan.FromMinutes(8)); + var client = new AppInsightsClient(Configuration); + await RetryAssertUntilTelemetryShouldBeAvailableAsync( + async () => await assertion(client), + timeout: TimeSpan.FromMinutes(5)); + } + else if (TestLocation is TestLocation.Local) + { + var client = new InMemoryTelemetryQueryClient(_telemetrySink); + await RetryAssertUntilTelemetryShouldBeAvailableAsync( + async () => await assertion(client), + TimeSpan.FromSeconds(5)); } } - private ApplicationInsightsDataClient CreateApplicationInsightsClient() - { - var clientCredentials = new ApiKeyClientCredentials(Configuration.GetValue("ApplicationInsights:ApiKey")); - var client = new ApplicationInsightsDataClient(clientCredentials); - - return client; - } - - private static async Task RetryAssertUntilTelemetryShouldBeAvailableAsync(Func assertion, TimeSpan timeout) + private async Task RetryAssertUntilTelemetryShouldBeAvailableAsync(Func assertion, TimeSpan timeout) { - Exception lastException = null; - PolicyResult result = - await Policy.TimeoutAsync(timeout) - .WrapAsync(Policy.Handle() - .WaitAndRetryForeverAsync(index => TimeSpan.FromSeconds(1))) - .ExecuteAndCaptureAsync(async () => - { - try - { - await assertion(); - } - catch (Exception exception) - { - lastException = exception; - throw; - } - }); - - if (result.Outcome is OutcomeType.Failure) + await Poll.UntilAvailableAsync(assertion, options => { - if (result.FinalException is TimeoutRejectedException - && result.FinalException.InnerException != null - && result.FinalException.InnerException is not TaskCanceledException) - { - throw result.FinalException.InnerException; - } - - if (lastException != null) - { - throw lastException; - } - - throw result.FinalException; - } + options.Interval = TimeSpan.FromSeconds(1); + options.Timeout = timeout; + options.FailureMessage = $"({TestLocation}) Telemetry should be available in Application Insights but it wasn't within the given timeout"; + }); } } } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AzureFunctionsDockerTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AzureFunctionsDockerTests.cs index 4911dcb8..5a45bfd1 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AzureFunctionsDockerTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/AzureFunctionsDockerTests.cs @@ -1,5 +1,4 @@ -using System.Net.Http; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.Azure.ApplicationInsights.Query.Models; using Xunit; using Xunit.Abstractions; @@ -10,8 +9,6 @@ namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsight [Trait("Category", "Docker")] public class AzureFunctionsDockerTests : ApplicationInsightsSinkTests { - private static readonly HttpClient HttpClient = new HttpClient(); - /// /// Initializes a new instance of the class. /// @@ -23,6 +20,8 @@ public AzureFunctionsDockerTests(ITestOutputHelper outputWriter) [Fact] public async Task LogRequest_WithRequestsOperationName_SinksToApplicationInsights() { + TestLocation = TestLocation.Remote; + await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { EventsRequestResult[] results = await client.GetRequestsAsync(); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CosmosSqlDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CosmosSqlDependencyTests.cs index f0e47ab5..05fbcc0b 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CosmosSqlDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CosmosSqlDependencyTests.cs @@ -41,6 +41,7 @@ public async Task LogCosmosSqlDependency_SinksToApplicationInsights_ResultsInCos await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { EventsDependencyResult[] results = await client.GetDependenciesAsync(); + Assert.NotEmpty(results); AssertX.Any(results, result => { Assert.Equal(dependencyType, result.Dependency.Type); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomDependencyTests.cs index 04a5181a..c0c9fd17 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomDependencyTests.cs @@ -56,6 +56,8 @@ public async Task LogDependency_SinksToApplicationInsights_ResultsInDependencyTe DateTimeOffset startTime = DateTimeOffset.Now; TimeSpan duration = BogusGenerator.Date.Timespan(); Dictionary telemetryContext = CreateTestTelemetryContext(); + + TestLocation = TestLocation.Remote; // Act Logger.LogDependency(dependencyType, dependencyData, isSuccessful, dependencyName, startTime, duration, dependencyId, telemetryContext); @@ -70,6 +72,8 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Equal(dependencyData, result.Dependency.Data); Assert.Equal(dependencyName, result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); + Assert.Equal(isSuccessful, result.Success); + Assert.All(telemetryContext, item => Assert.Equal(item.Value.ToString(), Assert.Contains(item.Key, result.CustomDimensions))); }); }); } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomRequestTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomRequestTests.cs index 36ff9f76..147133df 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomRequestTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/CustomRequestTests.cs @@ -41,10 +41,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { Assert.Equal(operationName, result.Request.Name); Assert.Contains(customRequestSource, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); Assert.Equal(componentName, result.Cloud.RoleName); }); }); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsDependencyTests.cs index bd0e2ca8..d1570dfa 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsDependencyTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Arcus.Observability.Correlation; using Microsoft.Azure.ApplicationInsights.Query.Models; using Microsoft.Extensions.Logging; using Serilog; diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsRequestTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsRequestTests.cs index 4a4fb1dc..f481dc99 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsRequestTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventHubsRequestTests.cs @@ -45,10 +45,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Equal(operationName, result.Request.Name); Assert.Contains(eventHubsName, result.Request.Source); Assert.Contains(eventHubsNamespace, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); Assert.Equal(componentName, result.Cloud.RoleName); AssertContainsCustomDimension(result.CustomDimensions, EventHubs.Namespace, eventHubsNamespace); @@ -58,7 +57,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => }); } - private static void AssertContainsCustomDimension(EventsResultDataCustomDimensions customDimensions, string key, string expected) + private static void AssertContainsCustomDimension(IDictionary customDimensions, string key, string expected) { Assert.True(customDimensions.TryGetValue(key, out string actual), $"Cannot find {key} in custom dimensions: {String.Join(", ", customDimensions.Keys)}"); Assert.Equal(expected, actual); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventTests.cs index 403acda3..332a882f 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/EventTests.cs @@ -32,8 +32,11 @@ public async Task LogCustomEvent_SinksToApplicationInsights_ResultsInEventTeleme // Assert await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { - EventsCustomEventResult[] results = await client.GetEventsAsync(); - Assert.Contains(results, result => result.CustomEvent.Name == eventName); + EventsCustomEventResult[] results = await client.GetCustomEventsAsync(); + AssertX.Any(results, ev => + { + Assert.Equal(eventName, ev.Name); + }); }); } @@ -52,7 +55,11 @@ public async Task LogEventWithComponentName_SinksToApplicationInsights_ResultsIn await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { EventsTraceResult[] results = await client.GetTracesAsync(); - Assert.Contains(results, result => result.Trace.Message == message && result.Cloud.RoleName == componentName); + AssertX.Any(results, trace => + { + Assert.Equal(message, trace.Trace.Message); + Assert.Equal(componentName, trace.Cloud.RoleName); + }); }); } @@ -62,6 +69,7 @@ public async Task LogCustomEventWithVersion_SinksToApplicationInsights_ResultsIn // Arrange var eventName = "Update version"; LoggerConfiguration.Enrich.WithVersion(); + TestLocation = TestLocation.Remote; // Act Logger.LogCustomEvent(eventName); @@ -69,12 +77,12 @@ public async Task LogCustomEventWithVersion_SinksToApplicationInsights_ResultsIn // Assert await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { - EventsCustomEventResult[] events = await client.GetEventsAsync(); - Assert.Contains(events, ev => + EventsCustomEventResult[] events = await client.GetCustomEventsAsync(); + AssertX.Any(events, ev => { - return ev.CustomEvent.Name == eventName - && ev.CustomDimensions.TryGetValue(VersionEnricher.DefaultPropertyName, out string actualVersion) - && !String.IsNullOrWhiteSpace(actualVersion); + Assert.Equal(eventName, ev.Name); + string actualVersion = Assert.Contains(VersionEnricher.DefaultPropertyName, ev.CustomDimensions); + Assert.False(string.IsNullOrWhiteSpace(actualVersion), "enriched event version should not be blank"); }); }); } @@ -102,15 +110,12 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => AssertX.Any(traceEvents, trace => { Assert.Equal(message, trace.Trace.Message); - Assert.True(trace.CustomDimensions.TryGetValue(ContextProperties.Correlation.OperationId, out string actualOperationId), "Requires a operation ID in the custom dimensions"); - Assert.True(trace.CustomDimensions.TryGetValue(ContextProperties.Correlation.TransactionId, out string actualTransactionId), "Requires a transaction ID in the custom dimensions"); - Assert.True(trace.CustomDimensions.TryGetValue(ContextProperties.Correlation.OperationParentId, out string actualOperationParentId), "Requires a operation parent ID in the custom dimensions"); - - Assert.Equal(operationId, actualOperationId); - Assert.Equal(transactionId, actualTransactionId); - Assert.Equal(operationParentId, actualOperationParentId); - Assert.Equal(transactionId, trace.Operation.Id); - Assert.Equal(operationId, trace.Operation.ParentId); + Assert.Equal(operationId, Assert.Contains(ContextProperties.Correlation.OperationId, trace.CustomDimensions)); + Assert.Equal(transactionId, Assert.Contains(ContextProperties.Correlation.TransactionId, trace.CustomDimensions)); + Assert.Equal(operationParentId, Assert.Contains(ContextProperties.Correlation.OperationParentId, trace.CustomDimensions)); + + Assert.Equal(transactionId, trace.Operation.Id); + Assert.Equal(operationId, trace.Operation.ParentId); }); }); } @@ -138,15 +143,12 @@ public async Task LogEventWithKubernetesInfo_SinksToApplicationInsights_ResultsI await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { EventsTraceResult[] traceEvents = await client.GetTracesAsync(); - Assert.Contains(traceEvents, trace => + AssertX.Any(traceEvents, trace => { - return message == trace.Trace.Message - && trace.CustomDimensions.TryGetValue(ContextProperties.Kubernetes.NodeName, out string actualNodeName) - && nodeName == actualNodeName - && trace.CustomDimensions.TryGetValue(ContextProperties.Kubernetes.PodName, out string actualPodName) - && podName == actualPodName - && trace.CustomDimensions.TryGetValue(ContextProperties.Kubernetes.Namespace, out string actualNamespace) - && @namespace == actualNamespace; + Assert.Equal(message, trace.Trace.Message); + Assert.Equal(nodeName, Assert.Contains(ContextProperties.Kubernetes.NodeName, trace.CustomDimensions)); + Assert.Equal(podName, Assert.Contains(ContextProperties.Kubernetes.PodName, trace.CustomDimensions)); + Assert.Equal(@namespace, Assert.Contains(ContextProperties.Kubernetes.Namespace, trace.CustomDimensions)); }); }); } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ExceptionTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ExceptionTests.cs index 4d588b16..d8848925 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ExceptionTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ExceptionTests.cs @@ -6,7 +6,6 @@ using Microsoft.Azure.ApplicationInsights.Query.Models; using Microsoft.Extensions.Logging; using Serilog; -using Serilog.Events; using Xunit; using Xunit.Abstractions; @@ -60,8 +59,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => AssertX.Any(results, result => { Assert.Equal(exception.Message, result.Exception.OuterMessage); - Assert.True(result.CustomDimensions.TryGetValue($"Exception-{nameof(TestException.SpyProperty)}", out string actualProperty)); - Assert.Equal(expectedProperty, actualProperty); + Assert.Equal(expectedProperty, Assert.Contains($"Exception-{nameof(TestException.SpyProperty)}", result.CustomDimensions)); }); }); } @@ -76,6 +74,7 @@ public async Task LogExceptionWithCustomPropertyFormat_SinksToApplicationInsight string propertyFormat = "Exception.{0}"; ApplicationInsightsSinkOptions.Exception.IncludeProperties = true; ApplicationInsightsSinkOptions.Exception.PropertyFormat = propertyFormat; + TestLocation = TestLocation.Remote; // Act Logger.LogCritical(exception, exception.Message); @@ -86,11 +85,10 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsExceptionResult[] results = await client.GetExceptionsAsync(); AssertX.Any(results, result => { - string propertyName = String.Format(propertyFormat, nameof(TestException.SpyProperty)); + string propertyName = string.Format(propertyFormat, nameof(TestException.SpyProperty)); Assert.Equal(exception.Message, result.Exception.OuterMessage); - Assert.True(result.CustomDimensions.TryGetValue(propertyName, out string actualProperty)); - Assert.Equal(expectedProperty, actualProperty); + Assert.Equal(expectedProperty, Assert.Contains(propertyName, result.CustomDimensions)); }); }); } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/ITelemetryQueryClient.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/ITelemetryQueryClient.cs new file mode 100644 index 00000000..2f18b6f4 --- /dev/null +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/ITelemetryQueryClient.cs @@ -0,0 +1,263 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.ApplicationInsights.Query.Models +{ + /// + /// Represents a client to query telemetry data from the Azure Application Insights instance. + /// + public interface ITelemetryQueryClient + { + /// + /// Gets the tracked traces from the Azure Application Insights instance. + /// + Task GetTracesAsync(); + + /// + /// Gets the tracked metrics from the Azure Application Insights instance. + /// + Task GetMetricsAsync(string metricName); + + /// + /// Gets the tracked custom events from the Azure Application Insights instance. + /// + Task GetCustomEventsAsync(); + + /// + /// Gets the tracked requests from the Azure Application Insights instance. + /// + Task GetRequestsAsync(); + + /// + /// Gets the tracked dependencies from the Azure Application Insights instance. + /// + Task GetDependenciesAsync(); + + /// + /// Gets the tracked exceptions from the Azure Application Insights instance. + /// + Task GetExceptionsAsync(); + } + + public class EventsTraceResult + { + public EventsTraceResult(string message, string roleName, OperationResult operation, IDictionary customDimensions) + { + Trace = new TraceResult(message); + Cloud = new CloudResult(roleName); + Operation = operation; + CustomDimensions = customDimensions; + } + + public TraceResult Trace { get; } + public CloudResult Cloud { get; } + public OperationResult Operation { get; } + public IDictionary CustomDimensions { get; } + + public class TraceResult + { + /// + /// Initializes a new instance of the class. + /// + public TraceResult(string message) + { + Message = message; + } + + public string Message { get; } + } + } + + public class EventsCustomEventResult + { + /// + /// Initializes a new instance of the class. + /// + public EventsCustomEventResult(string name, string roleName, IDictionary customDimensions) + { + Name = name; + Cloud = new CloudResult(roleName); + CustomDimensions = new ReadOnlyDictionary(customDimensions); + } + + public string Name { get; } + public CloudResult Cloud { get; } + public IReadOnlyDictionary CustomDimensions { get; } + } + + public class EventsMetricsResult + { + public EventsMetricsResult(string name, double value, IDictionary customDimensions) + { + Name = name; + Value = value; + CustomDimensions = customDimensions; + } + + public string Name { get; } + public double Value { get; } + public IDictionary CustomDimensions { get; } + } + + public class EventsRequestResult + { + public EventsRequestResult( + string id, + string name, + string source, + string url, + bool success, + string resultCode, + string roleName, + OperationResult operation, + IDictionary customDimensions) + { + Request = new RequestResult(id, name, source, url, resultCode); + Cloud = new CloudResult(roleName); + Success = success; + Operation = operation; + CustomDimensions = customDimensions; + } + + public RequestResult Request { get; } + public CloudResult Cloud { get; } + public bool Success { get; } + public OperationResult Operation { get; } + public IDictionary CustomDimensions { get; } + + public class RequestResult + { + /// + /// Initializes a new instance of the class. + /// + public RequestResult(string id, string name, string source, string url, string resultCode) + { + Id = id; + Name = name; + Source = source; + Url = url; + ResultCode = resultCode; + } + public string Id { get; } + public string Name { get; } + public string Source { get; } + public string Url { get; } + public string ResultCode { get; } + } + } + + public class EventsDependencyResult + { + public EventsDependencyResult( + string id, + string type, + string target, + string data, + bool success, + int resultCode, + string name, + string roleName, + OperationResult operation, + IDictionary customDimensions) + { + Dependency = new DependencyResult(id, name, type, target, data); + Cloud = new CloudResult(roleName); + Success = success; + ResultCode = resultCode; + Operation = operation; + CustomDimensions = customDimensions; + } + + public DependencyResult Dependency { get; } + public CloudResult Cloud { get; } + public bool Success { get; } + public int ResultCode { get; } + public OperationResult Operation { get; } + public IDictionary CustomDimensions { get; } + + public class DependencyResult + { + /// + /// Initializes a new instance of the class. + /// + public DependencyResult(string id, string name, string type, string target, string data) + { + Id = id; + Name = name; + Type = type; + Target = target; + Data = data; + } + + public string Id { get; } + public string Name { get; } + public string Type { get; } + public string Target { get; } + public string Data { get; } + } + } + + public class EventsExceptionResult + { + /// + /// Initializes a new instance of the class. + /// + public EventsExceptionResult(string message, OperationResult operation, string roleName, IDictionary customDimensions) + { + Exception = new ExceptionResult(message); + Cloud = new CloudResult(roleName); + Operation = operation; + CustomDimensions = customDimensions; + } + + public ExceptionResult Exception { get; } + public OperationResult Operation { get; } + public CloudResult Cloud { get; } + public IDictionary CustomDimensions { get; } + + public class ExceptionResult + { + /// + /// Initializes a new instance of the class. + /// + public ExceptionResult(string message) + { + OuterMessage = message; + } + + public string OuterMessage { get; } + } + } + + public class CloudResult + { + public CloudResult(string roleName) + { + RoleName = roleName; + } + + public string RoleName { get; } + } + + public class OperationResult + { + public OperationResult(string id, string parentId) + { + Id = id; + ParentId = parentId; + } + + public OperationResult(string id, string parentId, string name) + { + Id = id; + ParentId = parentId; + Name = name; + } + + public string Id { get; } + public string Name { get; } + public string ParentId { get; } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryApplicationInsightsTelemetryConverter.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryApplicationInsightsTelemetryConverter.cs new file mode 100644 index 00000000..c2447740 --- /dev/null +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryApplicationInsightsTelemetryConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Arcus.Observability.Telemetry.Serilog.Sinks.ApplicationInsights.Configuration; +using Arcus.Observability.Telemetry.Serilog.Sinks.ApplicationInsights.Converters; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Serilog.Events; +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; + +namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights.Fixture +{ + public class InMemoryApplicationInsightsTelemetryConverter : TelemetryConverterBase + { + private readonly ConcurrentStack _telemetries = new(); + + public ApplicationInsightsSinkOptions Options { get; set; } + + public RequestTelemetry[] Requests => _telemetries.ToArray().OfType().ToArray(); + public DependencyTelemetry[] Dependencies => _telemetries.ToArray().OfType().ToArray(); + public EventTelemetry[] Events => _telemetries.ToArray().OfType().ToArray(); + public MetricTelemetry[] Metrics => _telemetries.ToArray().OfType().ToArray(); + public TraceTelemetry[] Traces => _telemetries.ToArray().OfType().ToArray(); + public ExceptionTelemetry[] Exceptions => _telemetries.ToArray().OfType().ToArray(); + + public override IEnumerable Convert(LogEvent logEvent, IFormatProvider formatProvider) + { + var converter = ApplicationInsightsTelemetryConverter.Create(Options); + + IEnumerable telemetries = converter.Convert(logEvent, formatProvider); + foreach (ITelemetry telemetry in telemetries) + { + _telemetries.Push(telemetry); + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryTelemetryQueryClient.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryTelemetryQueryClient.cs new file mode 100644 index 00000000..314c5536 --- /dev/null +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/Fixture/InMemoryTelemetryQueryClient.cs @@ -0,0 +1,119 @@ +using System.Linq; +using System.Threading.Tasks; +using GuardNet; +using Microsoft.Azure.ApplicationInsights.Query.Models; + +namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights.Fixture +{ + /// + /// Represents an in-memory implementation to query telemetry data from the Azure Application Insights instance. + /// + public class InMemoryTelemetryQueryClient : ITelemetryQueryClient + { + private readonly InMemoryApplicationInsightsTelemetryConverter _telemetrySink; + + /// + /// Initializes a new instance of the class. + /// + public InMemoryTelemetryQueryClient(InMemoryApplicationInsightsTelemetryConverter telemetrySink) + { + Guard.NotNull(telemetrySink, nameof(telemetrySink)); + _telemetrySink = telemetrySink; + } + + /// + /// Gets the tracked traces from the Azure Application Insights instance. + /// + public Task GetTracesAsync() + { + return Task.FromResult(_telemetrySink.Traces.Select(t => + { + return new EventsTraceResult( + t.Message, + t.Context.Cloud.RoleName, + new OperationResult( + t.Context.Operation.Id, + t.Context.Operation.ParentId, + t.Context.Operation.Name), + t.Properties); + }).ToArray()); + } + + /// + /// Gets the tracked metrics from the Azure Application Insights instance. + /// + public Task GetMetricsAsync(string metricName) + { + return Task.FromResult(_telemetrySink.Metrics.Where(m => m.Name == metricName).Select(m => new EventsMetricsResult(m.Name, m.Sum, m.Properties)).ToArray()); + } + + /// + /// Gets the tracked custom events from the Azure Application Insights instance. + /// + public Task GetCustomEventsAsync() + { + return Task.FromResult(_telemetrySink.Events.Select(e => new EventsCustomEventResult(e.Name, e.Context.Cloud.RoleName, e.Properties)).ToArray()); + } + + /// + /// Gets the tracked requests from the Azure Application Insights instance. + /// + public Task GetRequestsAsync() + { + return Task.FromResult(_telemetrySink.Requests.Select(r => + { + var operation = new OperationResult(r.Context.Operation.Id, r.Context.Operation.ParentId, r.Context.Operation.Name); + return new EventsRequestResult( + r.Id, + r.Name, + r.Source, + r.Url?.ToString(), + r.Success ?? false, + r.ResponseCode, + r.Context.Cloud.RoleName, + operation, + r.Properties); + }).ToArray()); + } + + /// + /// Gets the tracked dependencies from the Azure Application Insights instance. + /// + public Task GetDependenciesAsync() + { + return Task.FromResult(_telemetrySink.Dependencies.Select(d => + { + var operation = new OperationResult(d.Context.Operation.Id, d.Context.Operation.ParentId, d.Context.Operation.Name); + return new EventsDependencyResult(d.Id, + d.Type, + d.Target, + d.Data, + d.Success ?? false, + string.IsNullOrWhiteSpace(d.ResultCode) ? 0 : int.Parse(d.ResultCode), + d.Name, + d.Context.Cloud.RoleName, + operation, + d.Properties); + }).ToArray()); + } + + /// + /// Gets the tracked exceptions from the Azure Application Insights instance. + /// + public Task GetExceptionsAsync() + { + return Task.FromResult(_telemetrySink.Exceptions.Select(e => + { + var operation = new OperationResult(e.Context.Operation.Id, + e.Context.Operation.ParentId, + e.Context.Operation.Name); + + return new EventsExceptionResult( + e.Exception.Message, + operation, + e.Context.Cloud.RoleName, + e.Properties); + }).ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/HttpDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/HttpDependencyTests.cs index d85c046e..f8f51051 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/HttpDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/HttpDependencyTests.cs @@ -53,7 +53,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsDependencyResult[] results = await client.GetDependenciesAsync(); AssertX.Any(results, result => { - Assert.Equal(DependencyType, result.Dependency.Type); + Assert.Equal(DependencyType, result.Dependency.Type, StringComparer.OrdinalIgnoreCase); Assert.Equal(requestUri.Host, result.Dependency.Target); Assert.Equal($"{httpMethod} {requestUri.AbsolutePath}", result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); @@ -92,7 +92,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsDependencyResult[] results = await client.GetDependenciesAsync(); AssertX.Any(results, result => { - Assert.Equal(DependencyType, result.Dependency.Type); + Assert.Equal(DependencyType, result.Dependency.Type, StringComparer.OrdinalIgnoreCase); Assert.Equal(requestUri.Host, result.Dependency.Target); Assert.Equal($"{httpMethod} {requestUri.AbsolutePath}", result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); @@ -132,7 +132,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsDependencyResult[] results = await client.GetDependenciesAsync(); AssertX.Any(results, result => { - Assert.Equal(DependencyType, result.Dependency.Type); + Assert.Equal(DependencyType, result.Dependency.Type, StringComparer.OrdinalIgnoreCase); Assert.Equal(request.Host.Host, result.Dependency.Target); Assert.Equal($"{httpMethod} {request.Path}", result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/MetricTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/MetricTests.cs index b79c4d46..cad5bdfb 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/MetricTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/MetricTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Azure.ApplicationInsights.Query.Models; using Microsoft.Extensions.Logging; @@ -15,7 +14,7 @@ public MetricTests(ITestOutputHelper outputWriter) : base(outputWriter) } [Fact] - public async Task LogMetric_SinksToApplicationInsights_ResultsInMetricTelemetry() + public async Task LogCustomMetric_SinksToApplicationInsights_ResultsInMetricTelemetry() { // Arrange string metricName = "threshold"; @@ -23,40 +22,28 @@ public async Task LogMetric_SinksToApplicationInsights_ResultsInMetricTelemetry( Dictionary telemetryContext = CreateTestTelemetryContext(); // Act - Logger.LogMetric(metricName, metricValue, telemetryContext); + Logger.LogCustomMetric(metricName, metricValue, telemetryContext); // Assert await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => { - var bodySchema = new MetricsPostBodySchema( - id: Guid.NewGuid().ToString(), - parameters: new MetricsPostBodySchemaParameters("customMetrics/" + metricName)); - - MetricsResultsItem[] results = await client.GetMetricsAsync(bodySchema); - Assert.NotEmpty(results); + EventsMetricsResult[] results = await client.GetMetricsAsync(metricName); + AssertX.Any(results, metric => + { + Assert.Equal(metricName, metric.Name); + Assert.Equal(metricValue, metric.Value); + + ContainsTelemetryContext(telemetryContext, metric); + }); }); } - [Fact] - public async Task LogCustomMetric_SinksToApplicationInsights_ResultsInMetricTelemetry() + private static void ContainsTelemetryContext(Dictionary telemetryContext, EventsMetricsResult metric) { - // Arrange - string metricName = "threshold"; - double metricValue = 0.25; - Dictionary telemetryContext = CreateTestTelemetryContext(); - - // Act - Logger.LogCustomMetric(metricName, metricValue, telemetryContext); - - // Assert - await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => + Assert.All(telemetryContext, item => { - var bodySchema = new MetricsPostBodySchema( - id: Guid.NewGuid().ToString(), - parameters: new MetricsPostBodySchemaParameters("customMetrics/" + metricName)); - - MetricsResultsItem[] results = await client.GetMetricsAsync(bodySchema); - Assert.NotEmpty(results); + string actual = Assert.Contains(item.Key, metric.CustomDimensions); + Assert.Equal(item.Value.ToString(), actual); }); } } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/RequestTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/RequestTests.cs index 6f4b153e..32909644 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/RequestTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/RequestTests.cs @@ -145,6 +145,8 @@ public async Task LogRequest_SinksToApplicationInsightsWithResponseStatusCodeWit DateTimeOffset startTime = DateTimeOffset.Now; Dictionary telemetryContext = CreateTestTelemetryContext(); + TestLocation = TestLocation.Remote; + // Act Logger.LogRequest(request, (int) statusCode, operationName, startTime, duration, telemetryContext); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusDependencyTests.cs index 8db04714..1453a6e2 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusDependencyTests.cs @@ -97,7 +97,7 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => }); } - private static void AssertContainsCustomDimension(EventsResultDataCustomDimensions customDimensions, string key, string expected) + private static void AssertContainsCustomDimension(IDictionary customDimensions, string key, string expected) { Assert.True(customDimensions.TryGetValue(key, out string actual), $"Cannot find {key} in custom dimensions: {String.Join(", ", customDimensions.Keys)}"); Assert.Equal(expected, actual); diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusRequestTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusRequestTests.cs index 453b8053..4dcbbe02 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusRequestTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/ServiceBusRequestTests.cs @@ -46,10 +46,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Equal(operationName, result.Request.Name); Assert.Contains(queueName, result.Request.Source); Assert.Contains(serviceBusNamespace, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityType, ServiceBusEntityType.Queue.ToString()); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityName, queueName); @@ -86,10 +85,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Equal(operationName, result.Request.Name); Assert.Contains(queueName, result.Request.Source); Assert.Contains(serviceBusNamespace, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityType, ServiceBusEntityType.Queue.ToString()); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityName, queueName); @@ -125,10 +123,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Contains(topicName, result.Request.Source); Assert.Contains(serviceBusNamespace, result.Request.Source); Assert.Contains(serviceBusNamespaceSuffix, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityType, ServiceBusEntityType.Topic.ToString()); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityName, topicName); @@ -151,6 +148,8 @@ public async Task LogServiceBusRequest_SinksToApplicationInsights_ResultsInReque var entityType = BogusGenerator.PickRandom(); Dictionary telemetryContext = CreateTestTelemetryContext(); + TestLocation = TestLocation.Remote; + // Act Logger.LogServiceBusRequest(serviceBusNamespace, entityName, operationName, isSuccessful, duration, startTime, entityType, telemetryContext); @@ -163,10 +162,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => Assert.Equal(operationName, result.Request.Name); Assert.Contains(entityName, result.Request.Source); Assert.Contains(serviceBusNamespace, result.Request.Source); - Assert.Empty(result.Request.Url); + Assert.True(string.IsNullOrWhiteSpace(result.Request.Url), "request URL should be blank"); Assert.Equal(operationName, result.Operation.Name); - Assert.True(bool.TryParse(result.Request.Success, out bool success)); - Assert.Equal(isSuccessful, success); + Assert.Equal(isSuccessful, result.Success); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityType, entityType.ToString()); AssertContainsCustomDimension(result.CustomDimensions, ContextProperties.RequestTracking.ServiceBus.EntityName, entityName); @@ -175,10 +173,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => }); } - private static void AssertContainsCustomDimension(EventsResultDataCustomDimensions customDimensions, string key, string expected) + private static void AssertContainsCustomDimension(IDictionary customDimensions, string key, string expected) { - Assert.True(customDimensions.TryGetValue(key, out string actual), $"Cannot find {key} in custom dimensions: {String.Join(", ", customDimensions.Keys)}"); - Assert.Equal(expected, actual); + Assert.Equal(expected, Assert.Contains(key, customDimensions)); } } } diff --git a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/SqlDependencyTests.cs b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/SqlDependencyTests.cs index a834b242..84cc7304 100644 --- a/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/SqlDependencyTests.cs +++ b/src/Arcus.Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/SqlDependencyTests.cs @@ -40,9 +40,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsDependencyResult[] results = await client.GetDependenciesAsync(); AssertX.Any(results, result => { - Assert.Equal(dependencyType, result.Dependency.Type); + Assert.Equal(dependencyType, result.Dependency.Type, StringComparer.OrdinalIgnoreCase); Assert.Equal(serverName, result.Dependency.Target); - Assert.Contains($"{dependencyType}: {databaseName}", result.Dependency.Name); + Assert.Contains(databaseName, result.Dependency.Name); Assert.Contains(operationName, result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); }); @@ -76,9 +76,9 @@ await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => EventsDependencyResult[] results = await client.GetDependenciesAsync(); AssertX.Any(results, result => { - Assert.Equal(dependencyType, result.Dependency.Type); + Assert.Equal(dependencyType, result.Dependency.Type, StringComparer.OrdinalIgnoreCase); Assert.Equal(serverName, result.Dependency.Target); - Assert.Contains($"{dependencyType}: {databaseName}", result.Dependency.Name); + Assert.Contains(databaseName, result.Dependency.Name); Assert.Contains(operationName, result.Dependency.Name); Assert.Equal(dependencyId, result.Dependency.Id); }); diff --git a/src/Arcus.Observability.Tests.Integration/appsettings.json b/src/Arcus.Observability.Tests.Integration/appsettings.json index ce6ee5c4..59a41661 100644 --- a/src/Arcus.Observability.Tests.Integration/appsettings.json +++ b/src/Arcus.Observability.Tests.Integration/appsettings.json @@ -1,10 +1,18 @@ { - "ApplicationInsights": { - "InstrumentationKey": "#{ApplicationInsights.InstrumentationKey}#", - "ApiKey": "#{ApplicationInsights.ApiKey}#", - "ApplicationId": "#{ApplicationInsights.ApplicationId}#" + "Arcus": { + "TenantId": "#{Arcus.Observability.TenantId}#", + "ServicePrincipal": { + "ClientId": "#{Arcus.Observability.ServicePrincipal.ClientId}#", + "ClientSecret": "#{Arcus.Observability.ServicePrincipal.ClientSecret}#" + }, + "ApplicationInsights": { + "InstrumentationKey": "#{Arcus.Observability.ApplicationInsights.InstrumentationKey}#", + "LogAnalytics": { + "WorkspaceId": "#{Arcus.Observability.ApplicationInsights.LogAnalytics.WorkspaceId}#" + } + } }, "AzureFunctions": { - "HttpPort": "#{AzureFunctions.HttpPort}#" - } + "HttpPort": "#{AzureFunctions.HttpPort}#" + } } \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj b/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj index d6f32fc5..9bef7fd2 100644 --- a/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj +++ b/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj @@ -18,7 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - +