diff --git a/.github/workflows/ci-concurrency-md.yml b/.github/workflows/ci-concurrency-md.yml new file mode 100644 index 00000000000..f6898c7f9b2 --- /dev/null +++ b/.github/workflows/ci-concurrency-md.yml @@ -0,0 +1,33 @@ +# Syntax: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +# See also: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks + +# Description: This workflow exists to unblock documentation-only PRs. + +# IMPORTANT: This workflow MUST use the same 'name' and 'matrix' as the non -md workflow. + + +name: Coyote Concurrency Tests + +on: + push: + branches: [ 'main*' ] + paths-ignore: + - '**.md' + pull_request: + branches: [ 'main*' ] + paths: + - '**.md' + +jobs: + coyote-concurrency-tests: + + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + os: [ windows-latest, ubuntu-latest ] + version: [ net8.0 ] + project: [ OpenTelemetry.Tests, OpenTelemetry.Api.Tests ] + + runs-on: ${{ matrix.os }} + steps: + - run: 'echo "No build required"' diff --git a/.github/workflows/ci-concurrency.yml b/.github/workflows/ci-concurrency.yml new file mode 100644 index 00000000000..82060ae39f2 --- /dev/null +++ b/.github/workflows/ci-concurrency.yml @@ -0,0 +1,41 @@ +name: Coyote Concurrency Tests + +on: + push: + branches: [ 'main*' ] + paths-ignore: + - '**.md' + pull_request: + branches: [ 'main*' ] + paths-ignore: + - '**.md' + +jobs: + coyote-concurrency-tests: + + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + os: [ windows-latest, ubuntu-latest ] + version: [ net8.0 ] + project: [ OpenTelemetry.Tests, OpenTelemetry.Api.Tests ] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # fetching all + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + + - name: Run Coyote Tests + shell: pwsh + run: .\build\test-threadSafety.ps1 -testProjectName ${{ matrix.project }} -targetFramework ${{ matrix.version }} + + - name: Publish Artifacts + if: always() && !cancelled() + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.os }}-${{ matrix.project }}-${{ matrix.version }}-coyoteoutput + path: '**/*_CoyoteOutput.*' diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml deleted file mode 100644 index 4abdb8d86a3..00000000000 --- a/.github/workflows/examples.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Build examples - -on: - push: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - pull_request: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - -jobs: - build-test-stable: - strategy: - fail-fast: false # ensures the entire test matrix is run, even if one permutation fails - matrix: - os: [ windows-latest, ubuntu-latest ] - version: [ net462, net6.0, net7.0, net8.0 ] - exclude: - - os: ubuntu-latest - version: net462 - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # fetching all - - - name: Setup dotnet - uses: actions/setup-dotnet@v3 - - - name: Install dependencies - run: dotnet restore examples/Examples.sln - - - name: Build - run: dotnet build examples/Examples.sln --configuration Release --no-restore --property:ExposeExperimentalFeatures=false - - build-test-experimental: - strategy: - fail-fast: false # ensures the entire test matrix is run, even if one permutation fails - matrix: - os: [ windows-latest, ubuntu-latest ] - version: [ net462, net6.0, net7.0, net8.0 ] - exclude: - - os: ubuntu-latest - version: net462 - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # fetching all - - - name: Setup dotnet - uses: actions/setup-dotnet@v3 - - - name: Install dependencies - run: dotnet restore examples/Examples.sln - - - name: Build - run: dotnet build examples/Examples.sln --configuration Release --no-restore --property:ExposeExperimentalFeatures=true diff --git a/.gitignore b/.gitignore index af409279810..e06229e460f 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ ASALocalRun/ # Tempo files tempo-data/ + +# Coyote Rewrite Files +rewrite.coyote.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 6ca67d71ca7..13fb63b9f08 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,12 +27,13 @@ these packages even during major version bumps, so compatibility is not a concern here. --> - - - - - - + + + + + + + @@ -51,7 +52,7 @@ 3) The .NET runtime team provides extra backward compatibility guarantee to System.Diagnostics.DiagnosticSource even during major version bumps, so compatibility is not a concern here. --> - + @@ -67,32 +68,32 @@ This section covers packages that are **not** directly referenced by the NuGet packages published from this repository. For example, these packages are used in the tests, examples or referenced as "PrivateAssets", but not in the NuGet packages themselves. --> - + - - - - - - - - - + + + + + + + + + - + - - + + - - - - - + + + + + @@ -106,8 +107,8 @@ - - - + + + diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index b963dced2be..337b5953028 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -11,11 +11,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore .editorconfig = .editorconfig + .gitignore = .gitignore + .github\workflows\ci-concurrency.yml = .github\workflows\ci-concurrency.yml CONTRIBUTING.md = CONTRIBUTING.md - Directory.Packages.props = Directory.Packages.props - test\Directory.Packages.props = test\Directory.Packages.props - examples\Directory.Packages.props = examples\Directory.Packages.props - docs\Directory.Packages.props = docs\Directory.Packages.props global.json = global.json LICENSE = LICENSE NuGet.config = NuGet.config @@ -34,6 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7CB2F02E build\docfx.cmd = build\docfx.cmd build\docker-compose.net6.0.yml = build\docker-compose.net6.0.yml build\docker-compose.net7.0.yml = build\docker-compose.net7.0.yml + build\docker-compose.net8.0.yml = build\docker-compose.net8.0.yml build\finalize-publicapi.ps1 = build\finalize-publicapi.ps1 build\GlobalAttrExclusions.txt = build\GlobalAttrExclusions.txt build\opentelemetry-icon-color.png = build\opentelemetry-icon-color.png @@ -44,6 +43,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7CB2F02E build\RELEASING.md = build\RELEASING.md build\stylecop.json = build\stylecop.json build\test-aot-compatibility.ps1 = build\test-aot-compatibility.ps1 + build\test-threadSafety.ps1 = build\test-threadSafety.ps1 build\xunit.runner.json = build\xunit.runner.json EndProjectSection EndProject @@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testdata", "testdata", "{77C7929A-2EED-4AA6-8705-B5C443C8AA0F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{E359BB2B-9AEC-497D-B321-7DF2450C3B8E}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shims.OpenTracing", "src\OpenTelemetry.Shims.OpenTracing\OpenTelemetry.Shims.OpenTracing.csproj", "{AAC408FE-40EF-4479-97D9-697F2C1A0B28}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shims.OpenTracing.Tests", "test\OpenTelemetry.Shims.OpenTracing.Tests\OpenTelemetry.Shims.OpenTracing.Tests.csproj", "{49A7853F-5B6F-4B65-A781-7D29A1C92164}" @@ -90,6 +92,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ ProjectSection(SolutionItems) = preProject .github\workflows\ci-aot-md.yml = .github\workflows\ci-aot-md.yml .github\workflows\ci-aot.yml = .github\workflows\ci-aot.yml + .github\workflows\ci-concurrency.yml = .github\workflows\ci-concurrency.yml + .github\workflows\ci-concurrency-md.yml = .github\workflows\ci-concurrency-md.yml .github\workflows\ci-instrumentation-libraries-md.yml = .github\workflows\ci-instrumentation-libraries-md.yml .github\workflows\ci-instrumentation-libraries.yml = .github\workflows\ci-instrumentation-libraries.yml .github\workflows\ci-md.yml = .github\workflows\ci-md.yml @@ -118,10 +122,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D2E73927-5 ProjectSection(SolutionItems) = preProject test\Directory.Build.props = test\Directory.Build.props test\Directory.Build.targets = test\Directory.Build.targets + test\Directory.Packages.props = test\Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Grpc.Tests", "test\OpenTelemetry.Instrumentation.Grpc.Tests\OpenTelemetry.Instrumentation.Grpc.Tests.csproj", "{305E9DFD-E73B-4A28-8769-795C25551020}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Console", "examples\Console\Examples.Console.csproj", "{FF3E6E08-E8E4-4523-B526-847CD989279F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.AspNetCore", "examples\AspNetCore\Examples.AspNetCore.csproj", "{0935622B-9377-4056-8343-AE6ECDC274CF}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "test\Benchmarks\Benchmarks.csproj", "{DE9130A4-F30A-49D7-8834-41DE3021218B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.SqlClient.Tests", "test\OpenTelemetry.Instrumentation.SqlClient.Tests\OpenTelemetry.Instrumentation.SqlClient.Tests.csproj", "{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}" @@ -139,6 +148,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{2C7DD1DA-C229-4D9E-9AF0-BCD5CD3E4948}" ProjectSection(SolutionItems) = preProject examples\Directory.Build.props = examples\Directory.Build.props + examples\Directory.Packages.props = examples\Directory.Packages.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trace", "trace", "{5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}" @@ -153,9 +163,23 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "logs", "logs", "{3862190B-E docs\logs\getting-started-console\README.md = docs\logs\getting-started-console\README.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroserviceExample", "MicroserviceExample", "{4D492D62-5150-45F9-817F-C99562E364E2}" + ProjectSection(SolutionItems) = preProject + examples\MicroserviceExample\.dockerignore = examples\MicroserviceExample\.dockerignore + examples\MicroserviceExample\docker-compose.yml = examples\MicroserviceExample\docker-compose.yml + examples\MicroserviceExample\README.md = examples\MicroserviceExample\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "examples\MicroserviceExample\WebApi\WebApi.csproj", "{07336602-860B-4975-95DD-405D19C00901}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkerService", "examples\MicroserviceExample\WorkerService\WorkerService.csproj", "{FA7A6F67-1F2F-4855-890D-51B5829578A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utils", "examples\MicroserviceExample\Utils\Utils.csproj", "{5435517C-AEC5-4182-87AE-14E13D31525F}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB401DF1-FF5C-4055-886E-1183E832B2D6}" ProjectSection(SolutionItems) = preProject docs\Directory.Build.props = docs\Directory.Build.props + docs\Directory.Packages.props = docs\Directory.Packages.props docs\docfx.json = docs\docfx.json docs\toc.yml = docs\toc.yml EndProjectSection @@ -164,6 +188,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extending-the-sdk", "docs\t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.W3cTraceContext.Tests", "test\OpenTelemetry.Instrumentation.W3cTraceContext.Tests\OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj", "{EAAC5A3C-708A-4609-A21F-8E5221AB58F2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.GrpcService", "examples\GrpcService\Examples.GrpcService.csproj", "{DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-console", "docs\logs\getting-started-console\getting-started-console.csproj", "{B3F03725-23A0-4582-9526-F6A7E38F35CC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.InMemory", "src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj", "{9BCEA68B-50E2-4A3A-93E6-B51AF612BCC1}" @@ -363,6 +389,14 @@ Global {305E9DFD-E73B-4A28-8769-795C25551020}.Debug|Any CPU.Build.0 = Debug|Any CPU {305E9DFD-E73B-4A28-8769-795C25551020}.Release|Any CPU.ActiveCfg = Release|Any CPU {305E9DFD-E73B-4A28-8769-795C25551020}.Release|Any CPU.Build.0 = Release|Any CPU + {FF3E6E08-E8E4-4523-B526-847CD989279F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF3E6E08-E8E4-4523-B526-847CD989279F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF3E6E08-E8E4-4523-B526-847CD989279F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF3E6E08-E8E4-4523-B526-847CD989279F}.Release|Any CPU.Build.0 = Release|Any CPU + {0935622B-9377-4056-8343-AE6ECDC274CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0935622B-9377-4056-8343-AE6ECDC274CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0935622B-9377-4056-8343-AE6ECDC274CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0935622B-9377-4056-8343-AE6ECDC274CF}.Release|Any CPU.Build.0 = Release|Any CPU {DE9130A4-F30A-49D7-8834-41DE3021218B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DE9130A4-F30A-49D7-8834-41DE3021218B}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE9130A4-F30A-49D7-8834-41DE3021218B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -387,6 +421,18 @@ Global {0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Release|Any CPU.Build.0 = Release|Any CPU + {07336602-860B-4975-95DD-405D19C00901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07336602-860B-4975-95DD-405D19C00901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07336602-860B-4975-95DD-405D19C00901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07336602-860B-4975-95DD-405D19C00901}.Release|Any CPU.Build.0 = Release|Any CPU + {FA7A6F67-1F2F-4855-890D-51B5829578A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA7A6F67-1F2F-4855-890D-51B5829578A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA7A6F67-1F2F-4855-890D-51B5829578A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA7A6F67-1F2F-4855-890D-51B5829578A9}.Release|Any CPU.Build.0 = Release|Any CPU + {5435517C-AEC5-4182-87AE-14E13D31525F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5435517C-AEC5-4182-87AE-14E13D31525F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5435517C-AEC5-4182-87AE-14E13D31525F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5435517C-AEC5-4182-87AE-14E13D31525F}.Release|Any CPU.Build.0 = Release|Any CPU {FCDCF532-A163-40DA-80B7-7530AA1182C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FCDCF532-A163-40DA-80B7-7530AA1182C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {FCDCF532-A163-40DA-80B7-7530AA1182C4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -395,6 +441,10 @@ Global {EAAC5A3C-708A-4609-A21F-8E5221AB58F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EAAC5A3C-708A-4609-A21F-8E5221AB58F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAAC5A3C-708A-4609-A21F-8E5221AB58F2}.Release|Any CPU.Build.0 = Release|Any CPU + {DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED}.Release|Any CPU.Build.0 = Release|Any CPU {B3F03725-23A0-4582-9526-F6A7E38F35CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B3F03725-23A0-4582-9526-F6A7E38F35CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3F03725-23A0-4582-9526-F6A7E38F35CC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -560,12 +610,19 @@ Global {E69578EB-B456-4062-A645-877CD964528B} = {F1D0972B-38CF-49C2-9F4B-4C5DE02FB71D} {C1542297-8763-4DF4-957C-489ED771C21D} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} {D2E73927-5966-445C-94E9-EFE6F269C8D5} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} + {FF3E6E08-E8E4-4523-B526-847CD989279F} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} + {0935622B-9377-4056-8343-AE6ECDC274CF} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} {2C7DD1DA-C229-4D9E-9AF0-BCD5CD3E4948} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} {3862190B-E2C5-418E-AFDC-DB281FB5C705} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} + {4D492D62-5150-45F9-817F-C99562E364E2} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} + {07336602-860B-4975-95DD-405D19C00901} = {4D492D62-5150-45F9-817F-C99562E364E2} + {FA7A6F67-1F2F-4855-890D-51B5829578A9} = {4D492D62-5150-45F9-817F-C99562E364E2} + {5435517C-AEC5-4182-87AE-14E13D31525F} = {4D492D62-5150-45F9-817F-C99562E364E2} {CB401DF1-FF5C-4055-886E-1183E832B2D6} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} {FCDCF532-A163-40DA-80B7-7530AA1182C4} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} + {DB942F5A-D571-4DEA-B1A7-B6BE0E24E6ED} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} {B3F03725-23A0-4582-9526-F6A7E38F35CC} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {13C10C9A-07E8-43EB-91F5-C2B116FBE0FC} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {08D29501-F0A3-468F-B18D-BD1821A72383} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index 4b4123472a6..d84e98e61e5 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -8,7 +8,7 @@ - net7.0 + net8.0 diff --git a/build/test-threadSafety.ps1 b/build/test-threadSafety.ps1 new file mode 100644 index 00000000000..6694870b6b4 --- /dev/null +++ b/build/test-threadSafety.ps1 @@ -0,0 +1,34 @@ +param( + [Parameter()][string]$coyoteVersion="1.7.10", + [Parameter(Mandatory=$true)][string]$testProjectName, + [Parameter(Mandatory=$true)][string]$targetFramework, + [Parameter()][string]$categoryName="CoyoteConcurrencyTests", + [Parameter()][string]$configuration="Release" +) + +$env:OTEL_RUN_COYOTE_TESTS = 'true' + +$rootDirectory = Split-Path $PSScriptRoot -Parent + +Write-Host "Install Coyote CLI." +dotnet tool install --global Microsoft.Coyote.CLI + +Write-Host "Build $testProjectName project." +dotnet build "$rootDirectory/test/$testProjectName/$testProjectName.csproj" --configuration $configuration + +$artifactsPath = Join-Path $rootDirectory "test/$testProjectName/bin/$configuration/$targetFramework" + +Write-Host "Generate Coyote rewriting options JSON file." +$assemblies = Get-ChildItem $artifactsPath -Filter OpenTelemetry*.dll | ForEach-Object {$_.Name} + +$RewriteOptionsJson = @{} +[void]$RewriteOptionsJson.Add("AssembliesPath", $artifactsPath) +[void]$RewriteOptionsJson.Add("Assemblies", $assemblies) +$RewriteOptionsJson | ConvertTo-Json -Compress | Set-Content -Path "$rootDirectory/test/$testProjectName/rewrite.coyote.json" + +Write-Host "Run Coyote rewrite." +coyote rewrite "$rootDirectory/test/$testProjectName/rewrite.coyote.json" + +Write-Host "Execute re-written binary." +dotnet test "$artifactsPath/$testProjectName.dll" --framework $targetFramework --filter CategoryName=$categoryName + diff --git a/docs/logs/customizing-the-sdk/README.md b/docs/logs/customizing-the-sdk/README.md index e8612859127..1c2b20b9ed5 100644 --- a/docs/logs/customizing-the-sdk/README.md +++ b/docs/logs/customizing-the-sdk/README.md @@ -56,8 +56,11 @@ For more information on Processors, please review [Extending the SDK](../extendi [Resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) is the immutable representation of the entity producing the telemetry. -If no `Resource` is explicitly configured, the default is to use a resource -indicating this [Telemetry +If no `Resource` is explicitly configured, the +[default](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#semantic-attributes-with-sdk-provided-default-value) +is to use a resource indicating this +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +and [Telemetry SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk). The `SetResourceBuilder` method on `OpenTelemetryLoggerOptions` can be used to set a single `ResourceBuilder`. If `SetResourceBuilder` is called multiple diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index 92b627103e1..fb41c28cdd9 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -574,7 +574,9 @@ is the immutable representation of the entity producing the telemetry. If no `Resource` is explicitly configured, the [default](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#semantic-attributes-with-sdk-provided-default-value) is to use a resource indicating this -[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service). +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +and [Telemetry +SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk). The `ConfigureResource` method on `MeterProviderBuilder` can be used to set a configure the resource on the provider. When the provider is built, it automatically builds the final `Resource` from the configured `ResourceBuilder`. diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index d1db1d0c821..fd16648c820 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -289,8 +289,10 @@ writing custom exporters. is the immutable representation of the entity producing the telemetry. If no `Resource` is explicitly configured, the [default](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#semantic-attributes-with-sdk-provided-default-value) -resource is used to indicate the -[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service). +is to use a resource indicating this +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +and [Telemetry +SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk). The `ConfigureResource` method on `TracerProviderBuilder` can be used to configure the resource on the provider. `ConfigureResource` accepts an `Action` to configure the `ResourceBuilder`. Multiple calls to `ConfigureResource` can be diff --git a/examples/Examples.sln b/examples/Examples.sln deleted file mode 100644 index 7773293d014..00000000000 --- a/examples/Examples.sln +++ /dev/null @@ -1,67 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34302.71 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroserviceExample", "MicroserviceExample", "{196FD231-A91E-499B-AC97-7756752FFF2B}" - ProjectSection(SolutionItems) = preProject - ..\MicroserviceExample\.dockerignore = ..\MicroserviceExample\.dockerignore - ..\MicroserviceExample\docker-compose.yml = ..\MicroserviceExample\docker-compose.yml - ..\MicroserviceExample\README.md = ..\MicroserviceExample\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utils", "MicroserviceExample\Utils\Utils.csproj", "{8C276A35-FD69-4B87-84FF-3BAF238B127E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "MicroserviceExample\WebApi\WebApi.csproj", "{FD0FE601-113D-47D6-9220-3B7EDBBC3E24}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkerService", "MicroserviceExample\WorkerService\WorkerService.csproj", "{4AA3DDA3-765D-4F19-815B-5A8969AC5389}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.AspNetCore", "AspNetCore\Examples.AspNetCore.csproj", "{CE7324F3-E14C-4EEC-B0D5-9EB9173A459F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Console", "Console\Examples.Console.csproj", "{AB07B644-A164-416E-AA66-556DBE94956E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.GrpcService", "GrpcService\Examples.GrpcService.csproj", "{EC5BBA57-A9FC-4656-9455-83D3E2B54BDC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8C276A35-FD69-4B87-84FF-3BAF238B127E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C276A35-FD69-4B87-84FF-3BAF238B127E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C276A35-FD69-4B87-84FF-3BAF238B127E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C276A35-FD69-4B87-84FF-3BAF238B127E}.Release|Any CPU.Build.0 = Release|Any CPU - {FD0FE601-113D-47D6-9220-3B7EDBBC3E24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD0FE601-113D-47D6-9220-3B7EDBBC3E24}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD0FE601-113D-47D6-9220-3B7EDBBC3E24}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD0FE601-113D-47D6-9220-3B7EDBBC3E24}.Release|Any CPU.Build.0 = Release|Any CPU - {4AA3DDA3-765D-4F19-815B-5A8969AC5389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4AA3DDA3-765D-4F19-815B-5A8969AC5389}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4AA3DDA3-765D-4F19-815B-5A8969AC5389}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4AA3DDA3-765D-4F19-815B-5A8969AC5389}.Release|Any CPU.Build.0 = Release|Any CPU - {CE7324F3-E14C-4EEC-B0D5-9EB9173A459F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE7324F3-E14C-4EEC-B0D5-9EB9173A459F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE7324F3-E14C-4EEC-B0D5-9EB9173A459F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE7324F3-E14C-4EEC-B0D5-9EB9173A459F}.Release|Any CPU.Build.0 = Release|Any CPU - {AB07B644-A164-416E-AA66-556DBE94956E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB07B644-A164-416E-AA66-556DBE94956E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB07B644-A164-416E-AA66-556DBE94956E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB07B644-A164-416E-AA66-556DBE94956E}.Release|Any CPU.Build.0 = Release|Any CPU - {EC5BBA57-A9FC-4656-9455-83D3E2B54BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC5BBA57-A9FC-4656-9455-83D3E2B54BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC5BBA57-A9FC-4656-9455-83D3E2B54BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC5BBA57-A9FC-4656-9455-83D3E2B54BDC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {8C276A35-FD69-4B87-84FF-3BAF238B127E} = {196FD231-A91E-499B-AC97-7756752FFF2B} - {FD0FE601-113D-47D6-9220-3B7EDBBC3E24} = {196FD231-A91E-499B-AC97-7756752FFF2B} - {4AA3DDA3-765D-4F19-815B-5A8969AC5389} = {196FD231-A91E-499B-AC97-7756752FFF2B} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {565701CA-CAF6-459D-90F3-255A431B6047} - EndGlobalSection -EndGlobal diff --git a/examples/MicroserviceExample/WebApi/Dockerfile b/examples/MicroserviceExample/WebApi/Dockerfile index adb1443b70a..d74077a0a87 100644 --- a/examples/MicroserviceExample/WebApi/Dockerfile +++ b/examples/MicroserviceExample/WebApi/Dockerfile @@ -1,7 +1,7 @@ -ARG SDK_VERSION=7.0 +ARG SDK_VERSION=8.0 FROM mcr.microsoft.com/dotnet/sdk:${SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net7.0 +ARG PUBLISH_FRAMEWORK=net8.0 WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WebApi -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /out -p:IntegrationBuild=true diff --git a/examples/MicroserviceExample/WorkerService/Dockerfile b/examples/MicroserviceExample/WorkerService/Dockerfile index 42de22a9e31..dafc0049b46 100644 --- a/examples/MicroserviceExample/WorkerService/Dockerfile +++ b/examples/MicroserviceExample/WorkerService/Dockerfile @@ -1,7 +1,7 @@ -ARG SDK_VERSION=7.0 +ARG SDK_VERSION=8.0 FROM mcr.microsoft.com/dotnet/sdk:${SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net7.0 +ARG PUBLISH_FRAMEWORK=net8.0 WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WorkerService -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /out -p:IntegrationBuild=true diff --git a/global.json b/global.json index 16c870f7f47..0aca8b12938 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.100-rc.2.23502.2" + "version": "8.0.100" } } diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md index c4abeb17c12..7feacfdf7e3 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md @@ -3,8 +3,8 @@ ## Unreleased * Updated `Microsoft.Extensions.DependencyInjection.Abstractions` package - version to `8.0.0-rc.2.23479.6`. - ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015)) + version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) ## 1.7.0-alpha.1 diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 67cf8f478ff..94718f8c190 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -3,8 +3,8 @@ ## Unreleased * Updated `System.Diagnostics.DiagnosticSource` package version to - `8.0.0-rc.2.23479.6`. - ([#4959](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4959)) + `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) ## 1.7.0-alpha.1 diff --git a/src/OpenTelemetry.Api/Trace/TracerProvider.cs b/src/OpenTelemetry.Api/Trace/TracerProvider.cs index 50d033cae51..f293b843c62 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProvider.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProvider.cs @@ -28,7 +28,7 @@ namespace OpenTelemetry.Trace; /// public class TracerProvider : BaseProvider { - private ConcurrentDictionary? tracers = new(); + internal ConcurrentDictionary? Tracers = new(); /// /// Initializes a new instance of the class. @@ -55,7 +55,7 @@ public Tracer GetTracer( string name, string? version = null) { - var tracers = this.tracers; + var tracers = this.Tracers; if (tracers == null) { // Note: Returns a no-op Tracer once dispose has been called. @@ -68,7 +68,7 @@ public Tracer GetTracer( { lock (tracers) { - if (this.tracers == null) + if (this.Tracers == null) { // Note: We check here for a race with Dispose and return a // no-op Tracer in that case. @@ -93,7 +93,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - var tracers = Interlocked.CompareExchange(ref this.tracers, null, this.tracers); + var tracers = Interlocked.CompareExchange(ref this.Tracers, null, this.Tracers); if (tracers != null) { lock (tracers) @@ -114,7 +114,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private readonly record struct TracerKey + internal readonly record struct TracerKey { public readonly string Name; public readonly string? Version; diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..8b137891791 100644 --- a/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index 3032509ec82..6b801fc615c 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -3,8 +3,14 @@ ## Unreleased * Updated `Microsoft.Extensions.Hosting.Abstractions` package - version to `8.0.0-rc.2.23479.6`. - ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015)) + version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +* The `OpenTelemetryBuilder.WithMetrics` method will now register an + `IMetricsListener` named 'OpenTelemetry' into the `IServiceCollection` to + enable metric management via the new `Microsoft.Extensions.Diagnostics` .NET 8 + APIs. + ([#4958](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4958)) ## 1.7.0-alpha.1 diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs new file mode 100644 index 00000000000..51c8c0d1826 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs @@ -0,0 +1,120 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics; + +internal sealed class OpenTelemetryMetricsListener : IMetricsListener, IDisposable +{ + private readonly MeterProviderSdk meterProviderSdk; + private IObservableInstrumentsSource? observableInstrumentsSource; + + public OpenTelemetryMetricsListener(MeterProvider meterProvider) + { + var meterProviderSdk = meterProvider as MeterProviderSdk; + + Debug.Assert(meterProviderSdk != null, "meterProvider was not MeterProviderSdk"); + + this.meterProviderSdk = meterProviderSdk!; + + this.meterProviderSdk.OnCollectObservableInstruments += this.OnCollectObservableInstruments; + } + + public string Name => "OpenTelemetry"; + + public void Dispose() + { + this.meterProviderSdk.OnCollectObservableInstruments -= this.OnCollectObservableInstruments; + } + + public MeasurementHandlers GetMeasurementHandlers() + { + return new MeasurementHandlers() + { + ByteHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + ShortHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + IntHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + LongHandler = this.MeasurementRecordedLong, + FloatHandler = (instrument, value, tags, state) + => this.MeasurementRecordedDouble(instrument, value, tags, state), + DoubleHandler = this.MeasurementRecordedDouble, + }; + } + + public bool InstrumentPublished(Instrument instrument, out object? userState) + { + userState = this.meterProviderSdk.InstrumentPublished(instrument, listeningIsManagedExternally: true); + return userState != null; + } + + public void MeasurementsCompleted(Instrument instrument, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementsCompleted(instrument, userState); + } + else + { + meterProvider.MeasurementsCompletedSingleStream(instrument, userState); + } + } + + public void Initialize(IObservableInstrumentsSource source) + { + this.observableInstrumentsSource = source; + } + + private void OnCollectObservableInstruments() + { + this.observableInstrumentsSource?.RecordObservableInstruments(); + } + + private void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan> tagsRos, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementRecordedDouble(instrument, value, tagsRos, userState); + } + else + { + meterProvider.MeasurementRecordedDoubleSingleStream(instrument, value, tagsRos, userState); + } + } + + private void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan> tagsRos, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementRecordedLong(instrument, value, tagsRos, userState); + } + else + { + meterProvider.MeasurementRecordedLongSingleStream(instrument, value, tagsRos, userState); + } + } +} diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj index 0d589aae7da..cc6b1b078a1 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj @@ -10,6 +10,7 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs index 805808488a1..00af83db167 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs @@ -15,6 +15,7 @@ // using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; @@ -61,13 +62,13 @@ public OpenTelemetryBuilder ConfigureResource( Guard.ThrowIfNull(configure); this.Services.ConfigureOpenTelemetryMeterProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); this.Services.ConfigureOpenTelemetryTracerProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); this.Services.ConfigureOpenTelemetryLoggerProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); return this; } @@ -76,9 +77,15 @@ public OpenTelemetryBuilder ConfigureResource( /// Adds metric services into the builder. /// /// - /// Note: This is safe to be called multiple times and by library authors. + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. /// Only a single will be created for a given - /// . + /// . + /// This method automatically registers an named 'OpenTelemetry' into the . + /// /// /// The supplied for chaining /// calls. @@ -95,11 +102,9 @@ public OpenTelemetryBuilder WithMetrics() /// calls. public OpenTelemetryBuilder WithMetrics(Action configure) { - Guard.ThrowIfNull(configure); - - var builder = new MeterProviderBuilderBase(this.Services); - - configure(builder); + OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener( + this.Services, + configure); return this; } diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs new file mode 100644 index 00000000000..a34cea3ea3e --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs @@ -0,0 +1,81 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics; + +/// +/// Contains extension methods for registering OpenTelemetry metrics with an +/// instance. +/// +internal static class OpenTelemetryMetricsBuilderExtensions +{ + /// + /// Adds an OpenTelemetry named 'OpenTelemetry' to the . + /// + /// + /// Note: This is safe to be called multiple times and by library authors. + /// Only a single will be created for a given + /// . + /// + /// . + /// The supplied for chaining + /// calls. + public static IMetricsBuilder UseOpenTelemetry( + this IMetricsBuilder metricsBuilder) + => UseOpenTelemetry(metricsBuilder, b => { }); + + /// + /// Adds an OpenTelemetry named 'OpenTelemetry' to the . + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. + public static IMetricsBuilder UseOpenTelemetry( + this IMetricsBuilder metricsBuilder, + Action configure) + { + Guard.ThrowIfNull(metricsBuilder); + + RegisterMetricsListener(metricsBuilder.Services, configure); + + return metricsBuilder; + } + + internal static void RegisterMetricsListener( + IServiceCollection services, + Action configure) + { + Debug.Assert(services != null, "services was null"); + + Guard.ThrowIfNull(configure); + + var builder = new MeterProviderBuilderBase(services!); + + services!.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + configure(builder); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs index dff3081e9e3..d8254871576 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -27,7 +27,6 @@ internal sealed class AspNetCoreInstrumentation : IDisposable "Microsoft.AspNetCore.Hosting.HttpRequestIn", "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", - "Microsoft.AspNetCore.Mvc.BeforeAction", "Microsoft.AspNetCore.Diagnostics.UnhandledException", "Microsoft.AspNetCore.Hosting.UnhandledException", }; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs index 7c2879b29d3..53e4f8f17ec 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs @@ -18,7 +18,6 @@ using System.Diagnostics.Metrics; using System.Reflection; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation.AspNetCore; @@ -46,11 +45,10 @@ internal sealed class AspNetCoreMetrics : IDisposable private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; private readonly Meter meter; - internal AspNetCoreMetrics(AspNetCoreMetricsInstrumentationOptions options) + internal AspNetCoreMetrics() { - Guard.ThrowIfNull(options); this.meter = new Meter(InstrumentationName, InstrumentationVersion); - var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter, options); + var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter); this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent); this.diagnosticSourceSubscriber.Subscribe(); } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs deleted file mode 100644 index 2ec3ff8a92c..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics; -using Microsoft.Extensions.Configuration; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; - -namespace OpenTelemetry.Instrumentation.AspNetCore; - -/// -/// Options for metrics requests instrumentation. -/// -internal sealed class AspNetCoreMetricsInstrumentationOptions -{ - internal readonly HttpSemanticConvention HttpSemanticConvention; - - /// - /// Initializes a new instance of the class. - /// - public AspNetCoreMetricsInstrumentationOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) - { - } - - internal AspNetCoreMetricsInstrumentationOptions(IConfiguration configuration) - { - Debug.Assert(configuration != null, "configuration was null"); - - this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); - } -} - diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index 15e4180b1b3..67fa080bf8c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +* Removed support for `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. The + library will now emit only the + [stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) + semantic conventions. + ([#5066](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5066)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + * Removed the Activity Status Description that was being set during exceptions. Activity Status will continue to be reported as `Error`. This is a **breaking change**. `EnrichWithException` can be leveraged @@ -35,6 +45,27 @@ exception. The attribute value will be set to full name of exception type. * Fixed `network.protocol.version` attribute values to match the specification. ([#5007](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5007)) +* Calls to `/metrics` will now be included in the `http.server.request.duration` + metric. This change may affect Prometheus pull scenario if the Prometheus + server sends request to the scraping endpoint that contains `/metrics` in + path. + ([#5044](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5044)) + +* Fixes the `http.route` attribute for scenarios in which it was + previously missing or incorrect. Additionally, the `http.route` attribute + is now the same for both the metric and `Activity` emitted for a request. + Lastly, the `Activity.DisplayName` has been adjusted to have the format + `{http.request.method} {http.route}` to conform with [the specification](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name). + There remain scenarios when using conventional routing or Razor pages where + `http.route` is still incorrect. See [#5056](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5056) + and [#5057](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5057) + for more details. + ([#5026](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5026)) + +* Removed `network.protocol.name` from `http.server.request.duration` metric as + per spec. + ([#5049](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5049)) + ## 1.6.0-beta.2 Released 2023-Oct-26 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 9b141e938a8..f0ab3611c4a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -23,8 +23,8 @@ using System.Runtime.CompilerServices; #endif using Microsoft.AspNetCore.Http; -#if NET6_0_OR_GREATER -using Microsoft.AspNetCore.Mvc.Diagnostics; +#if !NETSTANDARD +using Microsoft.AspNetCore.Routing; #endif using OpenTelemetry.Context.Propagation; #if !NETSTANDARD2_0 @@ -32,7 +32,6 @@ #endif using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; @@ -41,7 +40,6 @@ internal class HttpInListener : ListenerHandler internal const string ActivityOperationName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; - internal const string OnMvcBeforeActionEvent = "Microsoft.AspNetCore.Mvc.BeforeAction"; internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; @@ -67,8 +65,6 @@ internal class HttpInListener : ListenerHandler private readonly PropertyFetcher beforeActionTemplateFetcher = new("Template"); #endif private readonly AspNetCoreInstrumentationOptions options; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; public HttpInListener(AspNetCoreInstrumentationOptions options) : base(DiagnosticSourceName) @@ -76,10 +72,6 @@ public HttpInListener(AspNetCoreInstrumentationOptions options) Guard.ThrowIfNull(options); this.options = options; - - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); } public override void OnEventWritten(string name, object payload) @@ -97,12 +89,6 @@ public override void OnEventWritten(string name, object payload) this.OnStopActivity(Activity.Current, payload); } - break; - case OnMvcBeforeActionEvent: - { - this.OnMvcBeforeAction(Activity.Current, payload); - } - break; case OnUnhandledHostingExceptionEvent: case OnUnHandledDiagnosticsExceptionEvent: @@ -202,79 +188,38 @@ public void OnStartActivity(Activity activity, object payload) #endif var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - activity.DisplayName = path; + activity.DisplayName = this.GetDisplayName(request.Method); - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.emitOldAttributes) - { - if (request.Host.HasValue) - { - activity.SetTag(SemanticConventions.AttributeNetHostName, request.Host.Host); + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md - if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) - { - activity.SetTag(SemanticConventions.AttributeNetHostPort, request.Host.Port); - } - } - - activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method); - activity.SetTag(SemanticConventions.AttributeHttpScheme, request.Scheme); - activity.SetTag(SemanticConventions.AttributeHttpTarget, path); - activity.SetTag(SemanticConventions.AttributeHttpUrl, GetUri(request)); - activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); + if (request.Host.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); - if (request.Headers.TryGetValue("User-Agent", out var values)) + if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); - } + activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port); } } - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - if (this.emitNewAttributes) + if (request.QueryString.HasValue) { - if (request.Host.HasValue) - { - activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); - - if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) - { - activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port); - } - } - - if (request.QueryString.HasValue) - { - // QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571 - activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); - } + // QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571 + activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); + } - if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method, out var httpMethod)) - { - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"); - activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method); - } + RequestMethodHelper.SetHttpMethodTag(activity, request.Method); - activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); - activity.SetTag(SemanticConventions.AttributeUrlPath, path); - activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); + activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); + activity.SetTag(SemanticConventions.AttributeUrlPath, path); + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); - if (request.Headers.TryGetValue("User-Agent", out var values)) + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); - } + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); } } @@ -302,15 +247,16 @@ public void OnStopActivity(Activity activity, object payload) var response = context.Response; - if (this.emitOldAttributes) +#if !NETSTANDARD + var routePattern = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; + if (!string.IsNullOrEmpty(routePattern)) { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + activity.DisplayName = this.GetDisplayName(context.Request.Method, routePattern); + activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); } +#endif - if (this.emitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); #if !NETSTANDARD2_0 if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) @@ -363,57 +309,6 @@ public void OnStopActivity(Activity activity, object payload) } } - public void OnMvcBeforeAction(Activity activity, object payload) - { - // We cannot rely on Activity.Current here - // There could be activities started by middleware - // after activity started by framework resulting in different Activity.Current. - // so, we need to first find the activity started by Asp.Net Core. - // For .net6.0 onwards we could use IHttpActivityFeature to get the activity created by framework - // var httpActivityFeature = context.Features.Get(); - // activity = httpActivityFeature.Activity; - // However, this will not work as in case of custom propagator - // we start a new activity during onStart event which is a sibling to the activity created by framework - // So, in that case we need to get the activity created by us here. - // we can do so only by looping through activity.Parent chain. - while (activity != null) - { - if (string.Equals(activity.OperationName, ActivityOperationName, StringComparison.Ordinal)) - { - break; - } - - activity = activity.Parent; - } - - if (activity == null) - { - return; - } - - if (activity.IsAllDataRequested) - { -#if !NET6_0_OR_GREATER - _ = this.beforeActionActionDescriptorFetcher.TryFetch(payload, out var actionDescriptor); - _ = this.beforeActionAttributeRouteInfoFetcher.TryFetch(actionDescriptor, out var attributeRouteInfo); - _ = this.beforeActionTemplateFetcher.TryFetch(attributeRouteInfo, out var template); -#else - var beforeActionEventData = payload as BeforeActionEventData; - var template = beforeActionEventData.ActionDescriptor?.AttributeRouteInfo?.Template; -#endif - if (!string.IsNullOrEmpty(template)) - { - // override the span name that was previously set to the path part of URL. - activity.DisplayName = template; - activity.SetTag(SemanticConventions.AttributeHttpRoute, template); - } - - // TODO: Should we get values from RouteData? - // private readonly PropertyFetcher beforeActionRouteDataFetcher = new PropertyFetcher("routeData"); - // var routeData = this.beforeActionRouteDataFetcher.Fetch(payload) as RouteData; - } - } - public void OnException(Activity activity, object payload) { if (activity.IsAllDataRequested) @@ -425,10 +320,7 @@ public void OnException(Activity activity, object payload) return; } - if (this.emitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName); - } + activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName); if (this.options.RecordException) { @@ -509,7 +401,18 @@ private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); return !string.IsNullOrEmpty(grpcMethod); } +#endif + + private string GetDisplayName(string httpMethod, string httpRoute = null) + { + var normalizedMethod = RequestMethodHelper.GetNormalizedHttpMethod(httpMethod); + + return string.IsNullOrEmpty(httpRoute) + ? normalizedMethod + : $"{normalizedMethod} {httpRoute}"; + } +#if !NETSTANDARD2_0 [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) { @@ -520,28 +423,15 @@ private void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); - if (this.emitOldAttributes) - { - if (context.Connection.RemoteIpAddress != null) - { - // TODO: This attribute was changed in v1.13.0 https://github.com/open-telemetry/opentelemetry-specification/pull/2614 - activity.SetTag(SemanticConventions.AttributeNetPeerIp, context.Connection.RemoteIpAddress.ToString()); - } - - activity.SetTag(SemanticConventions.AttributeNetPeerPort, context.Connection.RemotePort); - } + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/rpc/rpc-spans.md - if (this.emitNewAttributes) + if (context.Connection.RemoteIpAddress != null) { - if (context.Connection.RemoteIpAddress != null) - { - activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); - } - - activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); + activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); } + activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); if (validConversion) { diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index 5d8db405158..a47ee19858e 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -24,13 +24,11 @@ using Microsoft.AspNetCore.Routing; #endif using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; internal sealed class HttpInMetricsListener : ListenerHandler { - internal const string HttpServerDurationMetricName = "http.server.duration"; internal const string HttpServerRequestDurationMetricName = "http.server.request.duration"; internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; @@ -43,31 +41,13 @@ internal sealed class HttpInMetricsListener : ListenerHandler private static readonly object ErrorTypeHttpContextItemsKey = new(); private readonly Meter meter; - private readonly AspNetCoreMetricsInstrumentationOptions options; - private readonly Histogram httpServerDuration; private readonly Histogram httpServerRequestDuration; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; - internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstrumentationOptions options) + internal HttpInMetricsListener(string name, Meter meter) : base(name) { this.meter = meter; - this.options = options; - - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); - - if (this.emitOldAttributes) - { - this.httpServerDuration = meter.CreateHistogram(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests."); - } - - if (this.emitNewAttributes) - { - this.httpServerRequestDuration = meter.CreateHistogram(HttpServerRequestDurationMetricName, "s", "Duration of HTTP server requests."); - } + this.httpServerRequestDuration = meter.CreateHistogram(HttpServerRequestDurationMetricName, "s", "Duration of HTTP server requests."); } public override void OnEventWritten(string name, object payload) @@ -77,24 +57,13 @@ public override void OnEventWritten(string name, object payload) case OnUnhandledDiagnosticsExceptionEvent: case OnUnhandledHostingExceptionEvent: { - if (this.emitNewAttributes) - { - this.OnExceptionEventWritten(name, payload); - } + this.OnExceptionEventWritten(name, payload); } break; case OnStopEvent: { - if (this.emitOldAttributes) - { - this.OnEventWritten_Old(name, payload); - } - - if (this.emitNewAttributes) - { - this.OnEventWritten_New(name, payload); - } + this.OnStopEventWritten(name, payload); } break; @@ -106,7 +75,7 @@ public void OnExceptionEventWritten(string name, object payload) // We need to use reflection here as the payload type is not a defined public type. if (!TryFetchException(payload, out Exception exc) || !TryFetchHttpContext(payload, out HttpContext ctx)) { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnExceptionEventWritten), HttpServerDurationMetricName); + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnExceptionEventWritten), HttpServerRequestDurationMetricName); return; } @@ -127,56 +96,7 @@ static bool TryFetchHttpContext(object payload, out HttpContext ctx) => HttpContextPropertyFetcher.TryFetch(payload, out ctx) && ctx != null; } - public void OnEventWritten_Old(string name, object payload) - { - var context = payload as HttpContext; - - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); - return; - } - - // TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this. - // Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too). - // If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope. - if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics")) - { - return; - } - - TagList tags = default; - - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, context.Request.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, context.Request.Method)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - - if (context.Request.Host.HasValue) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetHostName, context.Request.Host.Host)); - - if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetHostPort, context.Request.Host.Port)); - } - } - -#if NET6_0_OR_GREATER - var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; - if (!string.IsNullOrEmpty(route)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRoute, route)); - } -#endif - - // We are relying here on ASP.NET Core to set duration before writing the stop event. - // https://github.com/dotnet/aspnetcore/blob/d6fa351048617ae1c8b47493ba1abbe94c3a24cf/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L449 - // TODO: Follow up with .NET team if we can continue to rely on this behavior. - this.httpServerDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags); - } - - public void OnEventWritten_New(string name, object payload) + public void OnStopEventWritten(string name, object payload) { var context = payload as HttpContext; if (context == null) @@ -185,31 +105,15 @@ public void OnEventWritten_New(string name, object payload) return; } - // TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this. - // Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too). - // If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope. - if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics")) - { - return; - } - TagList tags = default; // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolName, NetworkProtocolName)); tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - if (RequestMethodHelper.KnownMethods.TryGetValue(context.Request.Method, out var httpMethod)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER")); - } + + var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(context.Request.Method); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); #if NET6_0_OR_GREATER var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs index 6118642b2ed..c394a3590ee 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs @@ -15,8 +15,6 @@ // #if !NET8_0_OR_GREATER -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.AspNetCore; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; #endif @@ -46,22 +44,9 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( _ = TelemetryHelper.BoxedStatusCodes; _ = RequestMethodHelper.KnownMethods; - builder.ConfigureServices(services => - { - services.RegisterOptionsFactory(configuration => new AspNetCoreMetricsInstrumentationOptions(configuration)); - }); - builder.AddMeter(AspNetCoreMetrics.InstrumentationName); - builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(Options.DefaultName); - - // TODO: Add additional options to AspNetCoreMetricsInstrumentationOptions ? - // RecordException - probably doesn't make sense for metric instrumentation - // EnableGrpcAspNetCoreSupport - this instrumentation will also need to also handle gRPC requests - return new AspNetCoreMetrics(options); - }); + builder.AddInstrumentation(new AspNetCoreMetrics()); return builder; #endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md index c62dd0050d6..5f32153a444 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md @@ -90,31 +90,51 @@ public void ConfigureServices(IServiceCollection services) #### List of metrics produced -A different metric is emitted depending on whether a user opts-in to the new -Http Semantic Conventions using `OTEL_SEMCONV_STABILITY_OPT_IN`. - -* By default, the instrumentation emits the following metric. - - | Name | Instrument Type | Unit | Description | Attributes | - |-------|-----------------|------|-------------|------------| - | `http.server.duration` | Histogram | `ms` | Measures the duration of inbound HTTP requests. | http.flavor, http.scheme, http.method, http.status_code, net.host.name, net.host.port, http.route | - -* If user sets the environment variable to `http`, the instrumentation emits - the following metric. - - | Name | Instrument Type | Unit | Description | Attributes | - |-------|-----------------|------|-------------|------------| - | `http.server.request.duration` | Histogram | `s` | Measures the duration of inbound HTTP requests. | network.protocol.version, url.scheme, http.request.method, http.response.status_code, http.route | - - This metric is emitted in `seconds` as per the semantic convention. While - the convention [recommends using custom histogram buckets](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md) - , this feature is not yet available via .NET Metrics API. - A [workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) +When the application targets `.NET6.0` or `.NET7.0`, the instrumentation emits +the following metric: + +| Name | Details | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `http.server.request.duration` | [Specification](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpserverrequestduration) | + +Starting from `.NET8.0`, metrics instrumentation is natively implemented, and +the ASP.NET Core library has incorporated support for [built-in +metrics](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore) +following the OpenTelemetry semantic conventions. The library includes additional +metrics beyond those defined in the +[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md), +covering additional scenarios for ASP.NET Core users. When the application +targets `.NET8.0` and newer versions, the instrumentation library automatically +enables all `built-in` metrics by default. + +Note that the `AddAspNetCoreInstrumentation()` extension simplifies the process +of enabling all built-in metrics via a single line of code. Alternatively, for +more granular control over emitted metrics, you can utilize the `AddMeter()` +extension on `MeterProviderBuilder` for meters listed in +[built-in-metrics-aspnetcore](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore). +Using `AddMeter()` for metrics activation eliminates the need to take dependency +on the instrumentation library package and calling +`AddAspNetCoreInstrumentation()`. + +If you utilize `AddAspNetCoreInstrumentation()` and wish to exclude unnecessary +metrics, you can utilize +[Views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument) +to achieve this. + +**Note:** There is no difference in features or emitted metrics when enabling +metrics using `AddMeter()` or `AddAspNetCoreInstrumentation()` on `.NET8.0` and +newer versions. + +> **Note** +> The `http.server.request.duration` metric is emitted in `seconds` as + per the semantic convention. While the convention [recommends using custom + histogram + buckets](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md) + , this feature is not yet available via .NET Metrics API. A + [workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) has been included in OTel SDK starting version `1.6.0` which applies - recommended buckets by default for `http.server.request.duration`. - -* If user sets the environment variable to `http/dup`, the instrumentation - emits both `http.server.duration` and `http.server.request.duration`. + recommended buckets by default for `http.server.request.duration`. This + applies to all targeted frameworks. ## Advanced configuration diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md index a244edccb4f..2299a1573a9 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.6.0-beta.3 + +Released 2023-Nov-17 + ## 1.6.0-beta.2 Released 2023-Oct-26 diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index 48fcda3d101..c54b2eefd09 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +* Removed support for `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. The + library will now emit only the + [stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) + semantic conventions. + ([#5068](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5068)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + * Removed the Activity Status Description that was being set during exceptions. Activity Status will continue to be reported as `Error`. This is a **breaking change**. `EnrichWithException` can be leveraged @@ -44,6 +54,12 @@ * Fixed `network.protocol.version` attribute values to match the specification. ([#5006](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5006)) +* Set `network.protocol.version` value using the protocol version on the + received response. If the request fails without response, then + `network.protocol.version` attribute will not be set on Activity and + `http.client.request.duration` metric. + ([#5043](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5043)) + ## 1.6.0-beta.2 Released 2023-Oct-26 @@ -118,6 +134,10 @@ Released 2023-Oct-26 definition. ([#4990](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4990)) +* `dns.lookups.duration` metric is renamed to `dns.lookup.duration`. This change + impacts only users on `.NET8.0` or newer framework. + ([#5049](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5049)) + ## 1.5.1-beta.1 Released 2023-Jul-20 diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetricInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientMetricInstrumentationOptions.cs deleted file mode 100644 index a35b615eec6..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetricInstrumentationOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics; -using Microsoft.Extensions.Configuration; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; - -namespace OpenTelemetry.Instrumentation.Http; - -internal sealed class HttpClientMetricInstrumentationOptions -{ - internal readonly HttpSemanticConvention HttpSemanticConvention; - - /// - /// Initializes a new instance of the class. - /// - public HttpClientMetricInstrumentationOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) - { - } - - internal HttpClientMetricInstrumentationOptions(IConfiguration configuration) - { - Debug.Assert(configuration != null, "configuration was null"); - - this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); - } -} diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs index 2f4db9ec9b2..6472ab91cff 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs @@ -37,11 +37,10 @@ internal sealed class HttpClientMetrics : IDisposable /// /// Initializes a new instance of the class. /// - /// HttpClient metric instrumentation options. - public HttpClientMetrics(HttpClientMetricInstrumentationOptions options) + public HttpClientMetrics() { this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( - new HttpHandlerMetricsDiagnosticListener("HttpHandlerDiagnosticListener", options), + new HttpHandlerMetricsDiagnosticListener("HttpHandlerDiagnosticListener"), this.isEnabled, HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent); this.diagnosticSourceSubscriber.Subscribe(); diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs index 924f260a90e..68e08949228 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs @@ -25,7 +25,6 @@ using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.Http.Implementation; @@ -49,8 +48,6 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler private static readonly PropertyFetcher StopExceptionFetcher = new("Exception"); private static readonly PropertyFetcher StopRequestStatusFetcher = new("RequestTaskStatus"); private readonly HttpClientInstrumentationOptions options; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; static HttpHandlerDiagnosticListener() { @@ -68,10 +65,6 @@ public HttpHandlerDiagnosticListener(HttpClientInstrumentationOptions options) : base("HttpHandlerDiagnosticListener") { this.options = options; - - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); } public override void OnEventWritten(string name, object payload) @@ -168,52 +161,33 @@ public void OnStartActivity(Activity activity, object payload) ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); } - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.emitOldAttributes) + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod)) { - activity.SetTag(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme); - activity.SetTag(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)); - activity.SetTag(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); - if (!request.RequestUri.IsDefaultPort) - { - activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); - } - - activity.SetTag(SemanticConventions.AttributeHttpUrl, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)); + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod); } - - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - if (this.emitNewAttributes) + else { - if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod)) - { - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"); - activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method.Method); - } + // Set to default "_OTHER" as per spec. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"); + activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method.Method); + } - activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); - if (!request.RequestUri.IsDefaultPort) - { - activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); - } + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + if (!request.RequestUri.IsDefaultPort) + { + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); + } - activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(request.Version)); + activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - if (request.Headers.TryGetValues("User-Agent", out var userAgentValues)) + if (request.Headers.TryGetValues("User-Agent", out var userAgentValues)) + { + var userAgent = userAgentValues.FirstOrDefault(); + if (!string.IsNullOrEmpty(userAgent)) { - var userAgent = userAgentValues.FirstOrDefault(); - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); - } + activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); } } @@ -276,18 +250,11 @@ public void OnStopActivity(Activity activity, object payload) activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode)); } - if (this.emitOldAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } - - if (this.emitNewAttributes) + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version)); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + if (activity.Status == ActivityStatusCode.Error) { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - if (activity.Status == ActivityStatusCode.Error) - { - activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode)); - } + activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode)); } try @@ -341,10 +308,7 @@ public void OnException(Activity activity, object payload) return; } - if (this.emitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc)); - } + activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc)); if (this.options.RecordException) { diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs index 9b20cbb533e..b3d45af15e8 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs @@ -25,7 +25,6 @@ using System.Reflection; using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.Http.Implementation; @@ -38,7 +37,6 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler internal static readonly string MeterVersion = AssemblyName.Version.ToString(); internal static readonly Meter Meter = new(MeterName, MeterVersion); private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception"; - private static readonly Histogram HttpClientDuration = Meter.CreateHistogram("http.client.duration", "ms", "Measures the duration of outbound HTTP requests."); private static readonly Histogram HttpClientRequestDuration = Meter.CreateHistogram("http.client.request.duration", "s", "Duration of HTTP client requests."); private static readonly PropertyFetcher StopRequestFetcher = new("Request"); @@ -49,27 +47,16 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler private static readonly HttpRequestOptionsKey HttpRequestOptionsErrorKey = new HttpRequestOptionsKey(SemanticConventions.AttributeErrorType); #endif - private readonly HttpClientMetricInstrumentationOptions options; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; - - public HttpHandlerMetricsDiagnosticListener(string name, HttpClientMetricInstrumentationOptions options) + public HttpHandlerMetricsDiagnosticListener(string name) : base(name) { - this.options = options; - - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); } public override void OnEventWritten(string name, object payload) { if (name == OnUnhandledExceptionEvent) { - if (this.emitNewAttributes) - { - this.OnExceptionEventWritten(Activity.Current, payload); - } + this.OnExceptionEventWritten(Activity.Current, payload); } else if (name == OnStopEvent) { @@ -86,90 +73,61 @@ public void OnStopEventWritten(Activity activity, object payload) if (TryFetchRequest(payload, out HttpRequestMessage request)) { - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.emitOldAttributes) - { - TagList tags = default; - - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host)); + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md + TagList tags = default; - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port)); - } - - if (TryFetchResponse(payload, out HttpResponseMessage response)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode))); - } - - // We are relying here on HttpClient library to set duration before writing the stop event. - // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178 - // TODO: Follow up with .NET team if we can continue to rely on this behavior. - HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags); + if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); } - - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - if (this.emitNewAttributes) + else { - TagList tags = default; + // Set to default "_OTHER" as per spec. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER")); + } - if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER")); - } + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(request.Version))); + if (!request.RequestUri.IsDefaultPort) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port)); + } - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port)); - } + if (TryFetchResponse(payload, out HttpResponseMessage response)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode))); - if (TryFetchResponse(payload, out HttpResponseMessage response)) + // Set error.type to status code for failed requests + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes + if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode) == ActivityStatusCode.Error) { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode))); - - // Set error.type to status code for failed requests - // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes - if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode) == ActivityStatusCode.Error) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode))); - } + tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode))); } + } - if (response == null) - { + if (response == null) + { #if !NET6_0_OR_GREATER - request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType); + request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType); #else - request.Options.TryGetValue(HttpRequestOptionsErrorKey, out var errorType); + request.Options.TryGetValue(HttpRequestOptionsErrorKey, out var errorType); #endif - // Set error.type to exception type if response was not received. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes - if (errorType != null) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, errorType)); - } + // Set error.type to exception type if response was not received. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes + if (errorType != null) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, errorType)); } - - // We are relying here on HttpClient library to set duration before writing the stop event. - // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178 - // TODO: Follow up with .NET team if we can continue to rely on this behavior. - HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags); } + + // We are relying here on HttpClient library to set duration before writing the stop event. + // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178 + // TODO: Follow up with .NET team if we can continue to rely on this behavior. + HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags); } // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs index 892f6fa6c5a..08c50229db8 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs @@ -28,12 +28,10 @@ internal static class HttpTagHelper private static readonly ConcurrentDictionary MethodOperationNameCache = new(); private static readonly ConcurrentDictionary HttpMethodOperationNameCache = new(); private static readonly ConcurrentDictionary HttpMethodNameCache = new(); - private static readonly ConcurrentDictionary ProtocolVersionToStringCache = new(); private static readonly Func ConvertMethodToOperationNameRef = ConvertMethodToOperationName; private static readonly Func ConvertHttpMethodToOperationNameRef = ConvertHttpMethodToOperationName; private static readonly Func ConvertHttpMethodToNameRef = ConvertHttpMethodToName; - private static readonly Func ConvertProtocolVersionToStringRef = ConvertProtocolVersionToString; /// /// Gets the OpenTelemetry standard name for an activity based on its Http method. @@ -56,13 +54,6 @@ internal static class HttpTagHelper /// Span method name. public static string GetNameForHttpMethod(HttpMethod method) => HttpMethodNameCache.GetOrAdd(method, ConvertHttpMethodToNameRef); - /// - /// Gets the OpenTelemetry standard version tag value for a span based on its protocol . - /// - /// . - /// Span flavor value. - public static string GetFlavorTagValueFromProtocolVersion(Version protocolVersion) => ProtocolVersionToStringCache.GetOrAdd(protocolVersion, ConvertProtocolVersionToStringRef); - /// /// Gets the OpenTelemetry standard uri tag value for a span based on its request . /// @@ -92,6 +83,4 @@ public static string GetUriTagValueFromRequestUri(Uri uri) private static string ConvertHttpMethodToOperationName(HttpMethod method) => $"HTTP {method}"; private static string ConvertHttpMethodToName(HttpMethod method) => method.ToString(); - - private static string ConvertProtocolVersionToString(Version protocolVersion) => protocolVersion.ToString(); } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index 3852201fde7..800eb5cb106 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -25,7 +25,6 @@ using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.Http.Implementation; @@ -49,16 +48,9 @@ internal static class HttpWebRequestActivitySource private static readonly string Version = AssemblyName.Version.ToString(); private static readonly ActivitySource WebRequestActivitySource = new(ActivitySourceName, Version); private static readonly Meter WebRequestMeter = new(MeterName, Version); - private static readonly Histogram HttpClientDuration = WebRequestMeter.CreateHistogram("http.client.duration", "ms", "Measures the duration of outbound HTTP requests."); private static readonly Histogram HttpClientRequestDuration = WebRequestMeter.CreateHistogram("http.client.request.duration", "s", "Measures the duration of outbound HTTP requests."); private static HttpClientInstrumentationOptions tracingOptions; - private static HttpClientMetricInstrumentationOptions metricsOptions; - - private static bool tracingEmitOldAttributes; - private static bool tracingEmitNewAttributes; - private static bool metricsEmitOldAttributes; - private static bool metricsEmitNewAttributes; // Fields for reflection private static FieldInfo connectionGroupListField; @@ -96,7 +88,6 @@ static HttpWebRequestActivitySource() PerformInjection(); TracingOptions = new HttpClientInstrumentationOptions(); - MetricsOptions = new HttpClientMetricInstrumentationOptions(); } catch (Exception ex) { @@ -111,21 +102,6 @@ internal static HttpClientInstrumentationOptions TracingOptions set { tracingOptions = value; - - tracingEmitOldAttributes = value.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - tracingEmitNewAttributes = value.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); - } - } - - internal static HttpClientMetricInstrumentationOptions MetricsOptions - { - get => metricsOptions; - set - { - metricsOptions = value; - - metricsEmitOldAttributes = value.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - metricsEmitNewAttributes = value.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); } } @@ -136,46 +112,27 @@ private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, A if (activity.IsAllDataRequested) { - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (tracingEmitOldAttributes) + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method, out var httpMethod)) { - activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method); - activity.SetTag(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); - if (!request.RequestUri.IsDefaultPort) - { - activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); - } - - activity.SetTag(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme); - activity.SetTag(SemanticConventions.AttributeHttpUrl, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion)); + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod); } - - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - if (tracingEmitNewAttributes) + else { - if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method, out var httpMethod)) - { - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"); - activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method); - } - - activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); - if (!request.RequestUri.IsDefaultPort) - { - activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); - } + // Set to default "_OTHER" as per spec. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"); + activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method); + } - activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(request.ProtocolVersion)); + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + if (!request.RequestUri.IsDefaultPort) + { + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); } + activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); + try { TracingOptions.EnrichWithHttpWebRequest?.Invoke(activity, request); @@ -194,17 +151,8 @@ private static void AddResponseTags(HttpWebResponse response, Activity activity) if (activity.IsAllDataRequested) { - if (tracingEmitOldAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } - - if (tracingEmitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } - - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode)); + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.ProtocolVersion)); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); try { @@ -243,42 +191,15 @@ private static string GetErrorType(Exception exception) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddExceptionTags(Exception exception, Activity activity, out HttpStatusCode? statusCode) + private static void AddExceptionEvent(Exception exception, Activity activity) { Debug.Assert(activity != null, "Activity must not be null"); - statusCode = null; - if (!activity.IsAllDataRequested) { return; } - ActivityStatusCode status; - - if (exception is WebException wexc && wexc.Response is HttpWebResponse response) - { - statusCode = response.StatusCode; - - if (tracingEmitOldAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)statusCode); - } - - if (tracingEmitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, (int)statusCode); - } - - status = SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)statusCode); - } - else - { - status = ActivityStatusCode.Error; - } - - activity.SetStatus(status); - if (TracingOptions.RecordException) { activity.RecordException(exception); @@ -309,7 +230,7 @@ private static void ProcessRequest(HttpWebRequest request) var enableTracing = WebRequestActivitySource.HasListeners() && TracingOptions.EventFilterHttpWebRequest(request); - if (!enableTracing && !HttpClientDuration.Enabled && !HttpClientRequestDuration.Enabled) + if (!enableTracing && !HttpClientRequestDuration.Enabled) { // Tracing and metrics are not enabled, so we can skip generating signals // Propagation must still be done in such cases, to allow @@ -397,6 +318,8 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC { HttpStatusCode? httpStatusCode = null; string errorType = null; + Version protocolVersion = null; + ActivityStatusCode activityStatus = ActivityStatusCode.Unset; // Activity may be null if we are not tracing in these cases: // 1. No listeners @@ -413,13 +336,30 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC if (result is Exception ex) { errorType = GetErrorType(ex); - if (activity != null) + if (ex is WebException wexc && wexc.Response is HttpWebResponse response) { - AddExceptionTags(ex, activity, out httpStatusCode); + httpStatusCode = response.StatusCode; + protocolVersion = response.ProtocolVersion; + activityStatus = SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode); + if (activityStatus == ActivityStatusCode.Error) + { + // override the errorType to statusCode for failures. + errorType = TelemetryHelper.GetStatusCodeString(response.StatusCode); + } + + if (activity != null) + { + AddResponseTags(response, activity); + AddExceptionEvent(ex, activity); + } } - else if (ex is WebException wexc && wexc.Response is HttpWebResponse response) + else { - httpStatusCode = response.StatusCode; + activityStatus = ActivityStatusCode.Error; + if (activity != null) + { + AddExceptionEvent(ex, activity); + } } } else @@ -447,6 +387,7 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC } httpStatusCode = responseCopy.StatusCode; + protocolVersion = responseCopy.ProtocolVersion; } else { @@ -456,11 +397,14 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC } httpStatusCode = response.StatusCode; + protocolVersion = response.ProtocolVersion; } - if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)httpStatusCode.Value) == ActivityStatusCode.Error) + activityStatus = SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)httpStatusCode.Value); + + if (activityStatus == ActivityStatusCode.Error) { - // override the errorType to statusCode for failures. + // set the errorType to statusCode for failures. errorType = TelemetryHelper.GetStatusCodeString(httpStatusCode.Value); } } @@ -470,85 +414,66 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC HttpInstrumentationEventSource.Log.FailedProcessResult(ex); } - if (tracingEmitNewAttributes && errorType != null) + if (activity != null && activity.IsAllDataRequested) { - activity?.SetTag(SemanticConventions.AttributeErrorType, errorType); + activity.SetStatus(activityStatus); + if (errorType != null) + { + activity.SetTag(SemanticConventions.AttributeErrorType, errorType); + } } activity?.Stop(); - if (HttpClientDuration.Enabled || HttpClientRequestDuration.Enabled) + if (HttpClientRequestDuration.Enabled) { double durationS; - double durationMs; if (activity != null) { durationS = activity.Duration.TotalSeconds; - durationMs = activity.Duration.TotalMilliseconds; } else { var endTimestamp = Stopwatch.GetTimestamp(); durationS = (endTimestamp - startTimestamp) / (double)Stopwatch.Frequency; - durationMs = durationS * 1000; } - if (metricsEmitOldAttributes) - { - TagList tags = default; - - tags.Add(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion)); - tags.Add(SemanticConventions.AttributeHttpMethod, request.Method); - tags.Add(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme); - tags.Add(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); - } - - if (httpStatusCode.HasValue) - { - tags.Add(SemanticConventions.AttributeHttpStatusCode, (int)httpStatusCode.Value); - } + TagList tags = default; - HttpClientDuration.Record(durationMs, tags); + if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method, out var httpMethod)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); } - - if (metricsEmitNewAttributes) + else { - TagList tags = default; - - if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method, out var httpMethod)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); - } - else - { - // Set to default "_OTHER" as per spec. - // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER")); - } + // Set to default "_OTHER" as per spec. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER")); + } - tags.Add(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); - tags.Add(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme); - tags.Add(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(request.ProtocolVersion)); - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(SemanticConventions.AttributeServerPort, request.RequestUri.Port); - } + tags.Add(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + tags.Add(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme); + if (protocolVersion != null) + { + tags.Add(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(protocolVersion)); + } - if (httpStatusCode.HasValue) - { - tags.Add(SemanticConventions.AttributeHttpResponseStatusCode, (int)httpStatusCode.Value); - } + if (!request.RequestUri.IsDefaultPort) + { + tags.Add(SemanticConventions.AttributeServerPort, request.RequestUri.Port); + } - if (errorType != null) - { - tags.Add(SemanticConventions.AttributeErrorType, errorType); - } + if (httpStatusCode.HasValue) + { + tags.Add(SemanticConventions.AttributeHttpResponseStatusCode, (int)httpStatusCode.Value); + } - HttpClientRequestDuration.Record(durationS, tags); + if (errorType != null) + { + tags.Add(SemanticConventions.AttributeErrorType, errorType); } + + HttpClientRequestDuration.Record(durationS, tags); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs index 73b7ca44c0a..3d6acde2c24 100644 --- a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs @@ -15,9 +15,9 @@ // #if !NET8_0_OR_GREATER -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +#if !NETFRAMEWORK using OpenTelemetry.Instrumentation.Http; +#endif using OpenTelemetry.Instrumentation.Http.Implementation; #endif @@ -49,28 +49,12 @@ public static MeterProviderBuilder AddHttpClientInstrumentation( _ = TelemetryHelper.BoxedStatusCodes; _ = RequestMethodHelper.KnownMethods; - builder.ConfigureServices(services => - { - services.RegisterOptionsFactory(configuration => new HttpClientMetricInstrumentationOptions(configuration)); - }); - #if NETFRAMEWORK builder.AddMeter(HttpWebRequestActivitySource.MeterName); - - if (builder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder) - { - deferredMeterProviderBuilder.Configure((sp, builder) => - { - var options = sp.GetRequiredService>().Get(Options.DefaultName); - - HttpWebRequestActivitySource.MetricsOptions = options; - }); - } #else builder.AddMeter(HttpHandlerMetricsDiagnosticListener.MeterName); - builder.AddInstrumentation(sp => new HttpClientMetrics( - sp.GetRequiredService>().Get(Options.DefaultName))); + builder.AddInstrumentation(new HttpClientMetrics()); #endif return builder; #endif diff --git a/src/OpenTelemetry.Instrumentation.Http/README.md b/src/OpenTelemetry.Instrumentation.Http/README.md index 1331d7ad68b..8e502ad7968 100644 --- a/src/OpenTelemetry.Instrumentation.Http/README.md +++ b/src/OpenTelemetry.Instrumentation.Http/README.md @@ -99,31 +99,51 @@ to see how to enable this instrumentation in an ASP.NET application. #### List of metrics produced -A different metric is emitted depending on whether a user opts-in to the new -Http Semantic Conventions using `OTEL_SEMCONV_STABILITY_OPT_IN`. - -* By default, the instrumentation emits the following metric. - - | Name | Instrument Type | Unit | Description | - |-------|-----------------|------|-------------| - | `http.client.duration` | Histogram | `ms` | Measures the duration of outbound HTTP requests. | - -* If user sets the environment variable to `http`, the instrumentation emits - the following metric. - - | Name | Instrument Type | Unit | Description | - |-------|-----------------|------|-------------| - | `http.client.request.duration` | Histogram | `s` | Measures the duration of outbound HTTP requests. | - - This metric is emitted in `seconds` as per the semantic convention. While - the convention [recommends using custom histogram buckets](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md) - , this feature is not yet available via .NET Metrics API. - A [workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) +When the application targets `NETFRAMEWORK`, `.NET6.0` or `.NET7.0`, the +instrumentation emits the following metric: + +| Name | Details | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `http.client.request.duration` | [Specification](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) | + +Starting from `.NET8.0`, metrics instrumentation is natively implemented, and +the HttpClient library has incorporated support for [built-in +metrics](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net) +following the OpenTelemetry semantic conventions. The library includes additional +metrics beyond those defined in the +[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md), +covering additional scenarios for HttpClient users. When the application targets +`.NET8.0` and newer versions, the instrumentation library automatically enables +all `built-in` metrics by default. + +Note that the `AddHttpClientInstrumentation()` extension simplifies the process +of enabling all built-in metrics via a single line of code. Alternatively, for +more granular control over emitted metrics, you can utilize the `AddMeter()` +extension on `MeterProviderBuilder` for meters listed in +[built-in-metrics-system-net](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net). +Using `AddMeter()` for metrics activation eliminates the need to take dependency +on the instrumentation library package and calling +`AddHttpClientInstrumentation()`. + +If you utilize `AddHttpClientInstrumentation()` and wish to exclude unnecessary +metrics, you can utilize +[Views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument) +to achieve this. + +**Note:** There is no difference in features or emitted metrics when enabling +metrics using `AddMeter()` or `AddHttpClientInstrumentation()` on `.NET8.0` and +newer versions. + +> **Note** +> The `http.client.request.duration` metric is emitted in `seconds` as + per the semantic convention. While the convention [recommends using custom + histogram + buckets](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md) + , this feature is not yet available via .NET Metrics API. A + [workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) has been included in OTel SDK starting version `1.6.0` which applies - recommended buckets by default for `http.client.request.duration`. - -* If user sets the environment variable to `http/dup`, the instrumentation - emits both `http.client.duration` and `http.client.request.duration`. + recommended buckets by default for `http.client.request.duration`. This + applies to all targeted frameworks. ## Advanced configuration diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index fe47ff3defb..75ca992ec03 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -2,9 +2,13 @@ ## Unreleased +## 1.6.0-beta.3 + +Released 2023-Nov-17 + * Updated `Microsoft.Extensions.Configuration` and - `Microsoft.Extensions.Options` package version to `8.0.0-rc.2.23479.6`. - ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015)) + `Microsoft.Extensions.Options` package version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) ## 1.6.0-beta.2 diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index d95648b95ef..2ebf6ad5f07 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,10 +2,6 @@ ## Unreleased -* Updated `Microsoft.Extensions.Logging` package version to - `8.0.0-rc.2.23479.6`. - ([#4959](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4959)) - * The `AddService` `ResourceBuilder` extension method will now generate the same `service.instance.id` for the lifetime of a process when `autoGenerateServiceInstanceId` is `true`. @@ -34,8 +30,28 @@ ([#5021](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5021)) * Updated `Microsoft.Extensions.Logging.Configuration` package version to - `8.0.0-rc.2.23479.6`. - ([#5020](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5020)) + `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +* Updated `Microsoft.Extensions.Logging` package version to + `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +* Revert the default behavior of Metrics SDK for Delta aggregation. It would not + reclaim unused Metric Points by default. You can enable the SDK to reclaim + unused Metric Points by setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS` to `true` + before setting up the `MeterProvider`. + ([#5052](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5052)) + +* Update Metrics SDK to override the default histogram buckets for ASP.NET + (.NET Framework). + + Histogram metrics for the meter name `OpenTelemetry.Instrumentation.AspNet` + and instrument name `http.request.server.duration` which have their `Unit` + as `s` (second) will have their default histogram buckets as `[ 0.005, 0.01, + 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ]`. + ([#5063](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5063)) ## 1.7.0-alpha.1 diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 1cbba26f9f8..67a668cf3d5 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -347,6 +347,18 @@ public void LoggerProcessStateSkipped(string type, string reason) this.WriteEvent(51, type, reason); } + [Event(52, Message = "Instrument '{0}', Meter '{1}' has been deactivated.", Level = EventLevel.Informational)] + public void MetricInstrumentDeactivated(string instrumentName, string meterName) + { + this.WriteEvent(52, instrumentName, meterName); + } + + [Event(53, Message = "Instrument '{0}', Meter '{1}' has been removed.", Level = EventLevel.Informational)] + public void MetricInstrumentRemoved(string instrumentName, string meterName) + { + this.WriteEvent(53, instrumentName, meterName); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs index d6cd664b686..ea7d8dc91c8 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs @@ -318,7 +318,12 @@ protected override void OnEventSourceCreated(EventSource eventSource) /// Data of the EventSource event. protected override void OnEventWritten(EventWrittenEventArgs eventData) { - this.WriteEvent(eventData.Message, eventData.Payload); + // Note: The EventSource check here works around a bug in EventListener. + // See: https://github.com/open-telemetry/opentelemetry-dotnet/pull/5046 + if (eventData.EventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + this.WriteEvent(eventData.Message, eventData.Payload); + } } private void Dispose(bool disposing) diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs index 5776241441d..e4cf260aa16 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs @@ -50,15 +50,43 @@ public static class OpenTelemetryLoggingExtensions /// The supplied for call chaining. public static ILoggingBuilder AddOpenTelemetry( this ILoggingBuilder builder) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null); + + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// Optional configuration action. + /// The supplied for call chaining. + public static ILoggingBuilder AddOpenTelemetry( + this ILoggingBuilder builder, + Action? configure) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure); + + private static ILoggingBuilder AddOpenTelemetryInternal( + ILoggingBuilder builder, + Action? configureBuilder, + Action? configureOptions) { Guard.ThrowIfNull(builder); builder.AddConfiguration(); + var services = builder.Services; + + if (configureOptions != null) + { + // TODO: Move this below the RegisterLoggerProviderOptions call so + // that user-supplied delegate fires AFTER the options are bound to + // Logging:OpenTelemetry configuration. + services.Configure(configureOptions); + } + // Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions - RegisterLoggerProviderOptions(builder.Services); + RegisterLoggerProviderOptions(services); - new LoggerProviderBuilderBase(builder.Services).ConfigureBuilder( + var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder( (sp, logging) => { var options = sp.GetRequiredService>().CurrentValue; @@ -78,7 +106,9 @@ public static ILoggingBuilder AddOpenTelemetry( options.Processors.Clear(); }); - builder.Services.TryAddEnumerable( + configureBuilder?.Invoke(loggingBuilder); + + services.TryAddEnumerable( ServiceDescriptor.Singleton( sp => new OpenTelemetryLoggerProvider( sp.GetRequiredService(), @@ -107,23 +137,4 @@ static void RegisterLoggerProviderOptions(IServiceCollection services) LoggerProviderOptions.RegisterProviderOptions(services); } } - - /// - /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . - /// - /// - /// The to use. - /// Optional configuration action. - /// The supplied for call chaining. - public static ILoggingBuilder AddOpenTelemetry( - this ILoggingBuilder builder, - Action? configure) - { - if (configure != null) - { - builder.Services.Configure(configure); - } - - return AddOpenTelemetry(builder); - } } diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 9bbceacf6d6..42d1aa0443c 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -25,6 +25,7 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { internal readonly bool OutputDelta; + internal readonly bool ShouldReclaimUnusedMetricPoints; internal long DroppedMeasurements = 0; private static readonly string MetricPointCapHitFixMessage = "Consider opting in for the experimental SDK feature to emit all the throttled metrics under the overflow attribute by setting env variable OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE = true. You could also modify instrumentation to reduce the number of unique key/value pair combinations. Or use Views to drop unwanted tags. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."; @@ -81,6 +82,7 @@ internal AggregatorStore( AggregationTemporality temporality, int maxMetricPoints, bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, ExemplarFilter? exemplarFilter = null) { this.name = metricStreamIdentity.InstrumentName; @@ -122,7 +124,9 @@ internal AggregatorStore( reservedMetricPointsCount++; } - if (this.OutputDelta) + this.ShouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; + + if (this.OutputDelta && shouldReclaimUnusedMetricPoints) { this.availableMetricPoints = new Queue(maxMetricPoints - reservedMetricPointsCount); @@ -181,7 +185,7 @@ internal int Snapshot() this.batchSize = 0; if (this.OutputDelta) { - if (this.reclaimMetricPoints) + if (this.ShouldReclaimUnusedMetricPoints && this.reclaimMetricPoints) { this.SnapshotDeltaWithMetricPointReclaim(); } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index d5c979604bf..767cf30714b 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -277,6 +277,8 @@ public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterP /// The supplied for chaining. public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder) { + Guard.ThrowIfNull(resourceBuilder); + meterProviderBuilder.ConfigureBuilder((sp, builder) => { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) @@ -297,6 +299,8 @@ public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder /// The supplied for chaining. public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action configure) { + Guard.ThrowIfNull(configure); + meterProviderBuilder.ConfigureBuilder((sp, builder) => { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 917bb0b35f8..74405b02be2 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -30,8 +30,11 @@ internal sealed class MeterProviderSdk : MeterProvider internal readonly IDisposable? OwnedServiceProvider; internal int ShutdownCount; internal bool Disposed; + internal bool ShouldReclaimUnusedMetricPoints; + internal Action? OnCollectObservableInstruments; private const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + private const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; private readonly List instrumentations = new(); private readonly List> viewConfigs; @@ -39,6 +42,7 @@ internal sealed class MeterProviderSdk : MeterProvider private readonly MeterListener listener; private readonly MetricReader? reader; private readonly CompositeMetricReader? compositeMetricReader; + private readonly Func shouldListenTo = instrument => false; internal MeterProviderSdk( IServiceProvider serviceProvider, @@ -51,6 +55,7 @@ internal MeterProviderSdk( var config = serviceProvider!.GetRequiredService(); _ = config.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out bool isEmitOverflowAttributeKeySet); + _ = config.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ShouldReclaimUnusedMetricPoints); this.ServiceProvider = serviceProvider!; @@ -146,16 +151,15 @@ internal MeterProviderSdk( } // Setup Listener - Func shouldListenTo = instrument => false; if (state.MeterSources.Any(s => WildcardHelper.ContainsWildcard(s))) { var regex = WildcardHelper.GetWildcardRegex(state.MeterSources); - shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); + this.shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); } else if (state.MeterSources.Any()) { var meterSourcesToSubscribe = new HashSet(state.MeterSources, StringComparer.OrdinalIgnoreCase); - shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); + this.shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); } OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Listening to following meters = \"{string.Join(";", state.MeterSources)}\"."); @@ -165,116 +169,19 @@ internal MeterProviderSdk( OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Number of views configured = {viewConfigCount}."); + this.listener.InstrumentPublished = (instrument, listener) => + { + object? state = this.InstrumentPublished(instrument, listeningIsManagedExternally: false); + if (state != null) + { + listener.EnableMeasurementEvents(instrument, state); + } + }; + // We expect that all the readers to be added are provided before MeterProviderSdk is built. // If there are no readers added, we do not enable measurements for the instruments. if (viewConfigCount > 0) { - this.listener.InstrumentPublished = (instrument, listener) => - { - bool enabledMeasurements = false; - - if (!shouldListenTo(instrument)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); - return; - } - - try - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - - // Creating list with initial capacity as the maximum - // possible size, to avoid any array resize/copy internally. - // There may be excess space wasted, but it'll eligible for - // GC right after this method. - var metricStreamConfigs = new List(viewConfigCount); - for (var i = 0; i < viewConfigCount; ++i) - { - var viewConfig = this.viewConfigs[i]; - MetricStreamConfiguration? metricStreamConfig = null; - - try - { - metricStreamConfig = viewConfig(instrument); - - // The SDK provides some static MetricStreamConfigurations. - // For example, the Drop configuration. The static ViewId - // should not be changed for these configurations. - if (metricStreamConfig != null && !metricStreamConfig.ViewId.HasValue) - { - metricStreamConfig.ViewId = i; - } - - if (metricStreamConfig is HistogramConfiguration - && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) - { - metricStreamConfig = null; - - OpenTelemetrySdkEventSource.Log.MetricViewIgnored( - instrument.Name, - instrument.Meter.Name, - "The current SDK does not allow aggregating non-Histogram instruments as Histograms.", - "Fix the view configuration."); - } - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration."); - } - - if (metricStreamConfig != null) - { - metricStreamConfigs.Add(metricStreamConfig); - } - } - - if (metricStreamConfigs.Count == 0) - { - // No views matched. Add null - // which will apply defaults. - // Users can turn off this default - // by adding a view like below as the last view. - // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) - metricStreamConfigs.Add(null); - } - - if (this.reader != null) - { - if (this.compositeMetricReader == null) - { - var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs); - if (metrics.Count > 0) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } - } - else - { - var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs); - if (metricsSuperList.Any(metrics => metrics.Count > 0)) - { - listener.EnableMeasurementEvents(instrument, metricsSuperList); - enabledMeasurements = true; - } - } - } - - if (enabledMeasurements) - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); - } - else - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); - } - } - catch (Exception) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); - } - }; - // Everything double this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDouble); this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state)); @@ -289,92 +196,186 @@ internal MeterProviderSdk( } else { - this.listener.InstrumentPublished = (instrument, listener) => - { - bool enabledMeasurements = false; + // Everything double + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); + + // Everything long + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLongSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + + this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); + } + + this.listener.Start(); + + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); + } + + internal Resource Resource { get; } + + internal List Instrumentations => this.instrumentations; + + internal MetricReader? Reader => this.reader; + + internal int ViewCount => this.viewConfigs.Count; + + internal object? InstrumentPublished(Instrument instrument, bool listeningIsManagedExternally) + { + var listenToInstrumentUsingSdkConfiguration = this.shouldListenTo(instrument); + + if (listeningIsManagedExternally && listenToInstrumentUsingSdkConfiguration) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument belongs to a Meter which has been enabled both externally and via a subscription on the provider. External subscription will be ignored in favor of the provider subscription.", + "Programmatic calls adding meters to the SDK (either by calling AddMeter directly or indirectly through helpers such as 'AddInstrumentation' extensions) are always favored over external registrations. When also using external management (typically IMetricsBuilder or IMetricsListener) remove programmatic calls to the SDK to allow registrations to be added and removed dynamically."); + return null; + } + else if (!listenToInstrumentUsingSdkConfiguration && !listeningIsManagedExternally) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument belongs to a Meter not subscribed by the provider.", + "Use AddMeter to add the Meter to the provider."); + return null; + } - if (!shouldListenTo(instrument)) + object? state = null; + var viewConfigCount = this.viewConfigs.Count; + + try + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); + + if (viewConfigCount <= 0) + { + if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); - return; + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument name is invalid.", + "The name must comply with the OpenTelemetry specification"); + return null; } - try + if (this.reader != null) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - - if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) + if (this.compositeMetricReader == null) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( - instrument.Name, - instrument.Meter.Name, - "Instrument name is invalid.", - "The name must comply with the OpenTelemetry specification"); - - return; + state = this.reader.AddMetricWithNoViews(instrument); + } + else + { + var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); + if (metrics.Any(metric => metric != null)) + { + state = metrics; + } } + } + } + else + { + // Creating list with initial capacity as the maximum + // possible size, to avoid any array resize/copy internally. + // There may be excess space wasted, but it'll eligible for + // GC right after this method. + var metricStreamConfigs = new List(viewConfigCount); + for (var i = 0; i < viewConfigCount; ++i) + { + var viewConfig = this.viewConfigs[i]; + MetricStreamConfiguration? metricStreamConfig = null; - if (this.reader != null) + try { - if (this.compositeMetricReader == null) + metricStreamConfig = viewConfig(instrument); + + // The SDK provides some static MetricStreamConfigurations. + // For example, the Drop configuration. The static ViewId + // should not be changed for these configurations. + if (metricStreamConfig != null && !metricStreamConfig.ViewId.HasValue) { - var metric = this.reader.AddMetricWithNoViews(instrument); - if (metric != null) - { - listener.EnableMeasurementEvents(instrument, metric); - enabledMeasurements = true; - } + metricStreamConfig.ViewId = i; } - else + + if (metricStreamConfig is HistogramConfiguration + && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) { - var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); - if (metrics.Any(metric => metric != null)) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } + metricStreamConfig = null; + + OpenTelemetrySdkEventSource.Log.MetricViewIgnored( + instrument.Name, + instrument.Meter.Name, + "The current SDK does not allow aggregating non-Histogram instruments as Histograms.", + "Fix the view configuration."); } } - - if (enabledMeasurements) + catch (Exception ex) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); + OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration."); } - else + + if (metricStreamConfig != null) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); + metricStreamConfigs.Add(metricStreamConfig); } } - catch (Exception) + + if (metricStreamConfigs.Count == 0) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + // No views matched. Add null + // which will apply defaults. + // Users can turn off this default + // by adding a view like below as the last view. + // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) + metricStreamConfigs.Add(null); } - }; - - // Everything double - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); - // Everything long - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLongSingleStream); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + if (this.reader != null) + { + if (this.compositeMetricReader == null) + { + var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs); + if (metrics.Count > 0) + { + state = metrics; + } + } + else + { + var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs); + if (metricsSuperList.Any(metrics => metrics.Count > 0)) + { + state = metricsSuperList; + } + } + } + } - this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); + if (state != null) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); + return state; + } + else + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); + return null; + } + } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + return null; } - - this.listener.Start(); - - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); } - internal Resource Resource { get; } - - internal List Instrumentations => this.instrumentations; - - internal MetricReader? Reader => this.reader; - internal void MeasurementsCompletedSingleStream(Instrument instrument, object? state) { Debug.Assert(instrument != null, "instrument must be non-null."); @@ -539,6 +540,8 @@ internal void CollectObservableInstruments() try { this.listener.RecordObservableInstruments(); + + this.OnCollectObservableInstruments?.Invoke(); } catch (Exception exception) { diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index d515e98fe74..fc9d2bb9099 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -37,11 +37,12 @@ public sealed class Metric ("Microsoft.AspNetCore.RateLimiting", "aspnetcore.rate_limiting.request.time_in_queue"), ("Microsoft.AspNetCore.RateLimiting", "aspnetcore.rate_limiting.request_lease.duration"), ("Microsoft.AspNetCore.Server.Kestrel", "kestrel.tls_handshake.duration"), + ("OpenTelemetry.Instrumentation.AspNet", "http.server.request.duration"), ("OpenTelemetry.Instrumentation.AspNetCore", "http.server.request.duration"), ("OpenTelemetry.Instrumentation.Http", "http.client.request.duration"), ("System.Net.Http", "http.client.request.duration"), ("System.Net.Http", "http.client.request.time_in_queue"), - ("System.Net.NameResolution", "dns.lookups.duration"), + ("System.Net.NameResolution", "dns.lookup.duration"), }; // Long default histogram bounds. Not based on a standard. May change in the future. @@ -60,6 +61,7 @@ internal Metric( AggregationTemporality temporality, int maxMetricPointsPerMetricStream, bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, ExemplarFilter? exemplarFilter = null) { this.InstrumentIdentity = instrumentIdentity; @@ -166,9 +168,8 @@ internal Metric( throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, exemplarFilter); + this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); this.Temporality = temporality; - this.InstrumentDisposed = false; } /// @@ -211,7 +212,7 @@ internal Metric( /// internal MetricStreamIdentity InstrumentIdentity { get; private set; } - internal bool InstrumentDisposed { get; set; } + internal bool Active { get; set; } = true; /// /// Get the metric points for the metric stream. diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index ca146fe6031..577f5afec63 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -64,7 +64,7 @@ internal MetricPoint( Debug.Assert(aggregatorStore != null, "AggregatorStore was null."); Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null."); - if (aggregatorStore!.OutputDelta) + if (aggregatorStore!.OutputDelta && aggregatorStore.ShouldReclaimUnusedMetricPoints) { Debug.Assert(lookupData != null, "LookupData was null."); } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 8a310352f2b..0fede2ab672 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -16,6 +16,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using OpenTelemetry.Internal; @@ -50,20 +51,11 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfiguration: null); lock (this.instrumentCreationLock) { - if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric)) + if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric)) { return existingMetric; } - if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName)) - { - OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( - metricStreamIdentity.InstrumentName, - metricStreamIdentity.MeterName, - "Metric instrument has the same name as an existing one but differs by description, unit, or instrument type. Measurements from this instrument will still be exported but may result in conflicts.", - "Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict."); - } - var index = ++this.metricIndex; if (index >= this.maxMetricStreams) { @@ -75,7 +67,8 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) Metric? metric = null; try { - metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, this.exemplarFilter); + bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints; + metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter); } catch (NotSupportedException nse) { @@ -89,7 +82,9 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) this.instrumentIdentityToMetric[metricStreamIdentity] = metric; this.metrics![index] = metric; - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); + return metric; } } @@ -134,21 +129,12 @@ internal List AddMetricsListWithViews(Instrument instrument, List AddMetricsListWithViews(Instrument instrument, List metrics, double value, ReadOn internal void CompleteSingleStreamMeasurement(Metric metric) { - metric.InstrumentDisposed = true; + DeactivateMetric(metric); } internal void CompleteMeasurement(List metrics) { foreach (var metric in metrics) { - metric.InstrumentDisposed = true; + DeactivateMetric(metric); } } @@ -250,6 +238,41 @@ internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStre } } + private static void DeactivateMetric(Metric metric) + { + if (metric.Active) + { + // TODO: This will cause the metric to be removed from the storage + // array during the next collect/export. If this happens often we + // will run out of storage. Would it be better instead to set the + // end time on the metric and keep it around so it can be + // reactivated? + metric.Active = false; + + OpenTelemetrySdkEventSource.Log.MetricInstrumentDeactivated( + metric.Name, + metric.MeterName); + } + } + + private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric) + => this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric) + && existingMetric.Active; + + private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metricStreamIdentity) + { + if (!this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName)) + { + // TODO: If a metric is deactivated and then reactivated we log the + // same warning as if it was a duplicate. + OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( + metricStreamIdentity.InstrumentName, + metricStreamIdentity.MeterName, + "Metric instrument has the same name as an existing one but differs by description, unit, or instrument type. Measurements from this instrument will still be exported but may result in conflicts.", + "Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict."); + } + } + private Batch GetMetricsBatch() { Debug.Assert(this.metrics != null, "this.metrics was null"); @@ -262,25 +285,20 @@ private Batch GetMetricsBatch() int metricCountCurrentBatch = 0; for (int i = 0; i < target; i++) { - var metric = this.metrics![i]; - int metricPointSize = 0; + ref var metric = ref this.metrics![i]; if (metric != null) { - if (metric.InstrumentDisposed) - { - metricPointSize = metric.Snapshot(); - this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); - this.metrics[i] = null; - } - else - { - metricPointSize = metric.Snapshot(); - } + int metricPointSize = metric.Snapshot(); if (metricPointSize > 0) { this.metricsCurrentBatch![metricCountCurrentBatch++] = metric; } + + if (!metric.Active) + { + this.RemoveMetric(ref metric); + } } } @@ -292,4 +310,25 @@ private Batch GetMetricsBatch() return default; } } + + private void RemoveMetric(ref Metric? metric) + { + Debug.Assert(metric != null, "metric was null"); + + // TODO: This logic removes the metric. If the same + // metric is published again we will create a new metric + // for it. If this happens often we will run out of + // storage. Instead, should we keep the metric around + // and set a new start time + reset its data if it comes + // back? + + OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName); + + var result = this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); + Debug.Assert(result, "result was false"); + + // Note: metric is a reference to the array storage so + // this clears the metric out of the array. + metric = null; + } } diff --git a/src/Shared/RequestMethodHelper.cs b/src/Shared/RequestMethodHelper.cs index 8ae14f89d0b..f6050121b4b 100644 --- a/src/Shared/RequestMethodHelper.cs +++ b/src/Shared/RequestMethodHelper.cs @@ -17,11 +17,17 @@ #if NET8_0_OR_GREATER using System.Collections.Frozen; #endif +using System.Diagnostics; +using OpenTelemetry.Trace; namespace OpenTelemetry.Internal; internal static class RequestMethodHelper { + // The value "_OTHER" is used for non-standard HTTP methods. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes + public const string OtherHttpMethod = "_OTHER"; + #if NET8_0_OR_GREATER internal static readonly FrozenDictionary KnownMethods; #else @@ -50,4 +56,24 @@ static RequestMethodHelper() KnownMethods = knownMethodSet; #endif } + + public static string GetNormalizedHttpMethod(string method) + { + return KnownMethods.TryGetValue(method, out var normalizedMethod) + ? normalizedMethod + : OtherHttpMethod; + } + + public static void SetHttpMethodTag(Activity activity, string method) + { + if (KnownMethods.TryGetValue(method, out var normalizedMethod)) + { + activity?.SetTag(SemanticConventions.AttributeHttpRequestMethod, normalizedMethod); + } + else + { + activity?.SetTag(SemanticConventions.AttributeHttpRequestMethod, OtherHttpMethod); + activity?.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, method); + } + } } diff --git a/src/Shared/SpanHelper.cs b/src/Shared/SpanHelper.cs index 67e6dcbfd8d..393a0f2cbf4 100644 --- a/src/Shared/SpanHelper.cs +++ b/src/Shared/SpanHelper.cs @@ -32,12 +32,13 @@ internal static class SpanHelper /// Resolved span for the Http status code. public static ActivityStatusCode ResolveSpanStatusForHttpStatusCode(ActivityKind kind, int httpStatusCode) { - var upperBound = kind == ActivityKind.Client ? 399 : 499; - if (httpStatusCode >= 100 && httpStatusCode <= upperBound) + var lowerBound = kind == ActivityKind.Client ? 400 : 500; + var upperBound = 599; + if (httpStatusCode >= lowerBound && httpStatusCode <= upperBound) { - return ActivityStatusCode.Unset; + return ActivityStatusCode.Error; } - return ActivityStatusCode.Error; + return ActivityStatusCode.Unset; } } diff --git a/test/Benchmarks/README.md b/test/Benchmarks/README.md index b168f038dc8..db06040cae6 100644 --- a/test/Benchmarks/README.md +++ b/test/Benchmarks/README.md @@ -3,11 +3,11 @@ Navigate to `./test/Benchmarks` directory and run the following command: ```sh -dotnet run -c Release -f net7.0 -- -m +dotnet run -c Release -f net8.0 -- -m ``` [How to use console arguments](https://benchmarkdotnet.org/articles/guides/console-args.html) - `-m` enables MemoryDiagnoser and prints memory statistics - `-f` allows you to filter the benchmarks by their full name using glob patterns - - `dotnet run -c Release -f net7.0 -- -f *TraceBenchmarks*` + - `dotnet run -c Release -f net8.0 -- -f *TraceBenchmarks*` diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props index 91bbe459e06..575224321a8 100644 --- a/test/Directory.Packages.props +++ b/test/Directory.Packages.props @@ -1,7 +1,8 @@ - + + diff --git a/test/OpenTelemetry.Api.Tests/BaggageTests.cs b/test/OpenTelemetry.Api.Tests/BaggageTests.cs index 02532070139..8ffcafde42c 100644 --- a/test/OpenTelemetry.Api.Tests/BaggageTests.cs +++ b/test/OpenTelemetry.Api.Tests/BaggageTests.cs @@ -275,7 +275,7 @@ public async Task AsyncLocalTests() { Baggage.SetBaggage("key1", "value1"); - await InnerTask().ConfigureAwait(false); + await InnerTask(); Baggage.SetBaggage("key4", "value4"); diff --git a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj index 3b5edd93cb3..31c6c8eb3db 100644 --- a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj +++ b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj @@ -14,6 +14,7 @@ + @@ -25,5 +26,6 @@ runtime; build; native; contentfiles; analyzers + diff --git a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs index 1e2e1f4579b..3dfde198017 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs @@ -15,17 +15,22 @@ // using System.Diagnostics; +using Microsoft.Coyote; +using Microsoft.Coyote.SystematicTesting; +using OpenTelemetry.Tests; using Xunit; +using Xunit.Abstractions; namespace OpenTelemetry.Trace.Tests; public class TracerTest : IDisposable { - // TODO: This is only a basic test. This must cover the entire shim API scenarios. + private readonly ITestOutputHelper output; private readonly Tracer tracer; - public TracerTest() + public TracerTest(ITestOutputHelper output) { + this.output = output; this.tracer = TracerProvider.Default.GetTracer("tracername", "tracerversion"); } @@ -83,16 +88,16 @@ public async Task Tracer_StartRootSpan_StartsNewTrace() async Task DoSomeAsyncWork() { - await Task.Delay(10).ConfigureAwait(false); + await Task.Delay(10); using (tracer.GetTracer("tracername").StartRootSpan("RootSpan2")) { - await Task.Delay(10).ConfigureAwait(false); + await Task.Delay(10); } } using (tracer.GetTracer("tracername").StartActiveSpan("RootSpan1")) { - await DoSomeAsyncWork().ConfigureAwait(false); + await DoSomeAsyncWork(); } Assert.Equal(2, exportedItems.Count); @@ -309,6 +314,91 @@ public void TracerBecomesNoopWhenParentProviderIsDisposedTest() Assert.False(span3.IsRecording); } + [SkipUnlessEnvVarFoundFact("OTEL_RUN_COYOTE_TESTS")] + [Trait("CategoryName", "CoyoteConcurrencyTests")] + public void TracerConcurrencyTest() + { + var config = Configuration.Create() + .WithTestingIterations(100) + .WithMemoryAccessRaceCheckingEnabled(true); + + var test = TestingEngine.Create(config, InnerTest); + + test.Run(); + + this.output.WriteLine(test.GetReport()); + this.output.WriteLine($"Bugs, if any: {string.Join("\n", test.TestReport.BugReports)}"); + + var dir = Directory.GetCurrentDirectory(); + if (test.TryEmitReports(dir, $"{nameof(this.TracerConcurrencyTest)}_CoyoteOutput", out IEnumerable reportPaths)) + { + foreach (var reportPath in reportPaths) + { + this.output.WriteLine($"Execution Report: {reportPath}"); + } + } + + if (test.TryEmitCoverageReports(dir, $"{nameof(this.TracerConcurrencyTest)}_CoyoteOutput", out reportPaths)) + { + foreach (var reportPath in reportPaths) + { + this.output.WriteLine($"Coverage report: {reportPath}"); + } + } + + Assert.Equal(0, test.TestReport.NumOfFoundBugs); + + static void InnerTest() + { + var testTracerProvider = new TestTracerProvider + { + ExpectedNumberOfThreads = Math.Max(1, Environment.ProcessorCount / 2), + }; + + var tracers = testTracerProvider.Tracers; + + Assert.NotNull(tracers); + + Thread[] getTracerThreads = new Thread[testTracerProvider.ExpectedNumberOfThreads]; + for (int i = 0; i < testTracerProvider.ExpectedNumberOfThreads; i++) + { + getTracerThreads[i] = new Thread((object state) => + { + var testTracerProvider = state as TestTracerProvider; + + var id = Interlocked.Increment(ref testTracerProvider.NumberOfThreads); + var name = $"Tracer{id}"; + + if (id == testTracerProvider.ExpectedNumberOfThreads) + { + testTracerProvider.StartHandle.Set(); + } + else + { + testTracerProvider.StartHandle.WaitOne(); + } + + var tracer = testTracerProvider.GetTracer(name); + + Assert.NotNull(tracer); + }); + + getTracerThreads[i].Start(testTracerProvider); + } + + testTracerProvider.StartHandle.WaitOne(); + + testTracerProvider.Dispose(); + + foreach (var getTracerThread in getTracerThreads) + { + getTracerThread.Join(); + } + + Assert.Empty(tracers); + } + } + public void Dispose() { Activity.Current = null; @@ -319,4 +409,11 @@ private static bool IsNoopSpan(TelemetrySpan span) { return span.Activity == null; } + + private sealed class TestTracerProvider : TracerProvider + { + public int ExpectedNumberOfThreads; + public int NumberOfThreads; + public EventWaitHandle StartHandle = new ManualResetEvent(false); + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index 38e9a95ea48..240c8ad88c1 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -106,12 +106,12 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( { return new HttpResponseMessage(); }) - .Callback((r, ct) => + .Callback(async (r, ct) => { httpRequest = r; // We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method - httpRequestContent = r.Content.ReadAsByteArrayAsync(ct)?.Result; + httpRequestContent = await r.Content.ReadAsByteArrayAsync(ct); }) #else .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) @@ -124,7 +124,7 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( httpRequest = r; // We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method - httpRequestContent = await r.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + httpRequestContent = await r.Content.ReadAsByteArrayAsync(); }) #endif .Verifiable(); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs index 8f5c287d10d..75e0dd0e55d 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs @@ -63,12 +63,12 @@ public async Task TestRecoveryAfterFailedExport() endpoints.MapGrpcService(); }); })) - .StartAsync().ConfigureAwait(false); + .StartAsync(); var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5050") }; var codes = new[] { Grpc.Core.StatusCode.Unimplemented, Grpc.Core.StatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}").ConfigureAwait(false); + await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); var exportResults = new List(); var otlpExporter = new OtlpTraceExporter(new OtlpExporterOptions() { Endpoint = new Uri("http://localhost:4317") }); @@ -101,7 +101,7 @@ public async Task TestRecoveryAfterFailedExport() Assert.Equal(2, exportResults.Count); Assert.Equal(ExportResult.Success, exportResults[1]); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); } private class MockCollectorState diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 3de33a38cba..ea38fb59cf9 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -172,7 +172,7 @@ await RunPrometheusExporterMiddlewareIntegrationTest( path: null, configureBranchedPipeline: null, optionsName: null), - registerMeterProvider: false).ConfigureAwait(false); + registerMeterProvider: false); } [Fact] @@ -235,7 +235,7 @@ await RunPrometheusExporterMiddlewareIntegrationTest( configureBranchedPipeline: null, optionsName: null)), services => services.AddRouting(), - registerMeterProvider: false).ConfigureAwait(false); + registerMeterProvider: false); } private static async Task RunPrometheusExporterMiddlewareIntegrationTest( @@ -265,7 +265,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( configureServices?.Invoke(services); }) .Configure(configure)) - .StartAsync().ConfigureAwait(false); + .StartAsync(); var tags = new KeyValuePair[] { @@ -284,7 +284,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( counter.Add(0.99D, tags); } - using var response = await host.GetTestClient().GetAsync(path).ConfigureAwait(false); + using var response = await host.GetTestClient().GetAsync(path); var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -294,7 +294,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( Assert.True(response.Content.Headers.Contains("Last-Modified")); Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); - string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); var matches = Regex.Matches( content, @@ -318,7 +318,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( validateResponse?.Invoke(response); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); } } #endif diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs index 1b44b854d3c..70b886b5202 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs @@ -65,7 +65,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon { collectTasks[i] = Task.Run(async () => { - var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + var response = await exporter.CollectionManager.EnterCollect(); try { return new Response @@ -81,18 +81,18 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon }); } - await Task.WhenAll(collectTasks).ConfigureAwait(false); + await Task.WhenAll(collectTasks); Assert.Equal(1, runningCollectCount); - var firstResponse = collectTasks[0].Result; + var firstResponse = await collectTasks[0]; Assert.False(firstResponse.CollectionResponse.FromCache); for (int i = 1; i < collectTasks.Length; i++) { - Assert.Equal(firstResponse.ViewPayload, collectTasks[i].Result.ViewPayload); - Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[i].Result.CollectionResponse.GeneratedAtUtc); + Assert.Equal(firstResponse.ViewPayload, (await collectTasks[i]).ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, (await collectTasks[i]).CollectionResponse.GeneratedAtUtc); } counter.Add(100); @@ -100,7 +100,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon // This should use the cache and ignore the second counter update. var task = exporter.CollectionManager.EnterCollect(); Assert.True(task.IsCompleted); - var response = await task.ConfigureAwait(false); + var response = await task; try { if (cacheEnabled) @@ -129,7 +129,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon { collectTasks[i] = Task.Run(async () => { - var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + var response = await exporter.CollectionManager.EnterCollect(); try { return new Response @@ -145,20 +145,20 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon }); } - await Task.WhenAll(collectTasks).ConfigureAwait(false); + await Task.WhenAll(collectTasks); Assert.Equal(cacheEnabled ? 2 : 3, runningCollectCount); - Assert.NotEqual(firstResponse.ViewPayload, collectTasks[0].Result.ViewPayload); - Assert.NotEqual(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[0].Result.CollectionResponse.GeneratedAtUtc); + Assert.NotEqual(firstResponse.ViewPayload, (await collectTasks[0]).ViewPayload); + Assert.NotEqual(firstResponse.CollectionResponse.GeneratedAtUtc, (await collectTasks[0]).CollectionResponse.GeneratedAtUtc); - firstResponse = collectTasks[0].Result; + firstResponse = await collectTasks[0]; Assert.False(firstResponse.CollectionResponse.FromCache); for (int i = 1; i < collectTasks.Length; i++) { - Assert.Equal(firstResponse.ViewPayload, collectTasks[i].Result.ViewPayload); - Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[i].Result.CollectionResponse.GeneratedAtUtc); + Assert.Equal(firstResponse.ViewPayload, (await collectTasks[i]).ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, (await collectTasks[i]).CollectionResponse.GeneratedAtUtc); } } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 5ef88ad24a1..d964432009e 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -81,13 +81,13 @@ public void UriPrefixesInvalid() [Fact] public async Task PrometheusExporterHttpServerIntegration() { - await this.RunPrometheusExporterHttpServerIntegrationTest().ConfigureAwait(false); + await this.RunPrometheusExporterHttpServerIntegrationTest(); } [Fact] public async Task PrometheusExporterHttpServerIntegration_NoMetrics() { - await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true).ConfigureAwait(false); + await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true); } private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false) @@ -125,7 +125,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri } using HttpClient client = new HttpClient(); - using var response = await client.GetAsync($"{address}metrics").ConfigureAwait(false); + using var response = await client.GetAsync($"{address}metrics"); if (!skipMetrics) { @@ -135,7 +135,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri Assert.Matches( "^# TYPE counter_double_total counter\ncounter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n\n# EOF\n$".Replace('\'', '"'), - await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + await response.Content.ReadAsStringAsync()); } else { diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs index 0d686b895bd..1122f2f02d2 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs @@ -52,7 +52,7 @@ await RunMetricsTest( using var meter = new Meter(meterName); var counter = meter.CreateCounter("meter"); counter.Add(10); - }).ConfigureAwait(false); + }); Assert.Single(exportedItems); var metricPointsEnumerator = exportedItems[0].GetMetricPoints().GetEnumerator(); @@ -75,7 +75,7 @@ await RunMetricsTest( using var meter = new Meter(meterName); var counter = meter.CreateCounter("meter"); counter.Add(10); - }).ConfigureAwait(false); + }); Assert.Single(exportedItems); Assert.Equal(10, exportedItems[0].MetricPoints[0].GetSumLong()); @@ -96,13 +96,13 @@ private static async Task RunMetricsTest(Action configure, return Task.CompletedTask; }))) - .StartAsync().ConfigureAwait(false); + .StartAsync(); - using var response = await host.GetTestClient().GetAsync($"/{nameof(RunMetricsTest)}").ConfigureAwait(false); + using var response = await host.GetTestClient().GetAsync($"/{nameof(RunMetricsTest)}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); } } #endif diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj index c7b5e9c2ea9..e1d6d4fb496 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -4,6 +4,7 @@ $(TargetFrameworksForTests) disable + $(DefineConstants);BUILDING_HOSTING_TESTS @@ -17,6 +18,20 @@ + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs index 4d31768c2ff..43da25a08b4 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs @@ -75,4 +75,41 @@ public void ConfigureResourceTest() loggerProvider.Resource.Attributes, kvp => kvp.Key == "l_key1" && (string)kvp.Value == "l_value1"); } + + [Fact] + public void ConfigureResourceServiceProviderTest() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .ConfigureResource(r => r.AddDetector(sp => sp.GetRequiredService())) + .WithLogging() + .WithMetrics() + .WithTracing(); + + using var sp = services.BuildServiceProvider(); + + var tracerProvider = sp.GetRequiredService() as TracerProviderSdk; + var meterProvider = sp.GetRequiredService() as MeterProviderSdk; + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(tracerProvider); + Assert.NotNull(meterProvider); + Assert.NotNull(loggerProvider); + + Assert.Single(tracerProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + Assert.Single(meterProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + Assert.Single(loggerProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + } + + private sealed class TestResourceDetector : IResourceDetector + { + public Resource Detect() => ResourceBuilder.CreateEmpty().AddAttributes( + new Dictionary + { + ["key1"] = "value1", + }).Build(); + } } diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs new file mode 100644 index 00000000000..66f2c11ee97 --- /dev/null +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs @@ -0,0 +1,259 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Options; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Metrics.Tests; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Extensions.Hosting.Tests; + +public class OpenTelemetryMetricsBuilderExtensionsTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EnableMetricsTest(bool useWithMetricsStyle) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + using (var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureMetricsBuilder: builder => builder.EnableMetrics(meter.Name), + configureMeterProviderBuilder: builder => builder.AddInMemoryExporter(exportedItems))) + { + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + } + + AssertSingleMetricWithLongSum(exportedItems); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EnableMetricsWithAddMeterTest(bool useWithMetricsStyle) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + using (var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureMetricsBuilder: builder => builder.EnableMetrics(meter.Name), + configureMeterProviderBuilder: builder => builder + .AddSdkMeter(meter.Name) + .AddInMemoryExporter(exportedItems))) + { + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + } + + AssertSingleMetricWithLongSum(exportedItems); + } + + [Theory] + [InlineData(false, MetricReaderTemporalityPreference.Delta)] + [InlineData(true, MetricReaderTemporalityPreference.Delta)] + [InlineData(false, MetricReaderTemporalityPreference.Cumulative)] + [InlineData(true, MetricReaderTemporalityPreference.Cumulative)] + public void ReloadOfMetricsViaIConfigurationWithExportCleanupTest(bool useWithMetricsStyle, MetricReaderTemporalityPreference temporalityPreference) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + var source = new MemoryConfigurationSource(); + var memory = new MemoryConfigurationProvider(source); + var configuration = new ConfigurationRoot(new[] { memory }); + + using var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureAppConfiguration: (context, builder) => builder.AddConfiguration(configuration), + configureMeterProviderBuilder: builder => builder + .AddInMemoryExporter(exportedItems, reader => reader.TemporalityPreference = temporalityPreference)); + + var meterProvider = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + + meterProvider.ForceFlush(); + + Assert.Empty(exportedItems); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum(exportedItems); + + exportedItems.Clear(); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "false"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Note: When in Cumulative the metric shows up on the export + // immediately after being deactivated and then is ignored. + AssertSingleMetricWithLongSum(exportedItems); + + meterProvider.ForceFlush(); + exportedItems.Clear(); + Assert.Empty(exportedItems); + } + else + { + Assert.Empty(exportedItems); + } + + memory.Set($"Metrics:OpenTelemetry:EnabledMetrics:{meter.Name}:Default", "true"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum(exportedItems); + + var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); + + // Note: We currently log a duplicate warning anytime a metric is reactivated. + Assert.Single(duplicateMetricInstrumentEvents); + + var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); + + Assert.Single(metricInstrumentDeactivatedEvents); + + var metricInstrumentRemovedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 53); + + Assert.Single(metricInstrumentRemovedEvents); + } + + [Theory] + [InlineData(false, MetricReaderTemporalityPreference.Delta)] + [InlineData(true, MetricReaderTemporalityPreference.Delta)] + [InlineData(false, MetricReaderTemporalityPreference.Cumulative)] + [InlineData(true, MetricReaderTemporalityPreference.Cumulative)] + public void ReloadOfMetricsViaIConfigurationWithoutExportCleanupTest(bool useWithMetricsStyle, MetricReaderTemporalityPreference temporalityPreference) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + var source = new MemoryConfigurationSource(); + var memory = new MemoryConfigurationProvider(source); + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + var configuration = new ConfigurationRoot(new[] { memory }); + + using var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureAppConfiguration: (context, builder) => builder.AddConfiguration(configuration), + configureMeterProviderBuilder: builder => builder + .AddInMemoryExporter(exportedItems, reader => reader.TemporalityPreference = temporalityPreference)); + + var meterProvider = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "false"); + configuration.Reload(); + counter.Add(1); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + configuration.Reload(); + counter.Add(1); + + meterProvider.ForceFlush(); + + // Note: We end up with 2 of the same metric being exported. This is + // because the current behavior when something is deactivated is to + // remove the metric. The next publish creates a new metric. + Assert.Equal(2, exportedItems.Count); + + AssertMetricWithLongSum(exportedItems[0]); + AssertMetricWithLongSum(exportedItems[1]); + + exportedItems.Clear(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum( + exportedItems, + expectedValue: temporalityPreference == MetricReaderTemporalityPreference.Delta ? 1 : 2); + + var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); + + // Note: We currently log a duplicate warning anytime a metric is reactivated. + Assert.Single(duplicateMetricInstrumentEvents); + + var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); + + Assert.Single(metricInstrumentDeactivatedEvents); + + var metricInstrumentRemovedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 53); + + Assert.Single(metricInstrumentRemovedEvents); + } + + private static void AssertSingleMetricWithLongSum(List exportedItems, long expectedValue = 1) + { + Assert.Single(exportedItems); + + AssertMetricWithLongSum(exportedItems[0], expectedValue); + } + + private static void AssertMetricWithLongSum(Metric metric, long expectedValue = 1) + { + List metricPoints = new(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + + var metricPoint = metricPoints[0]; + Assert.Equal(expectedValue, metricPoint.GetSumLong()); + } +} diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs index 181dec9e367..2b447a816d8 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs @@ -37,9 +37,9 @@ public async Task AddOpenTelemetry_StartWithoutProvidersDoesNotThrow() var host = builder.Build(); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); } [Fact] @@ -75,9 +75,9 @@ public async Task AddOpenTelemetry_StartWithExceptionsThrows() var host = builder.Build(); - await Assert.ThrowsAsync(() => host.StartAsync()).ConfigureAwait(false); + await Assert.ThrowsAsync(() => host.StartAsync()); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); Assert.True(expectedInnerExceptionThrown); } @@ -172,11 +172,11 @@ public async Task AddOpenTelemetry_WithTracing_HostConfigurationHonoredTest() Assert.False(configureBuilderCalled); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); Assert.True(configureBuilderCalled); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); host.Dispose(); } @@ -295,11 +295,11 @@ public async Task AddOpenTelemetry_WithMetrics_HostConfigurationHonoredTest() Assert.False(configureBuilderCalled); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); Assert.True(configureBuilderCalled); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); host.Dispose(); } @@ -418,11 +418,11 @@ public async Task AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() Assert.False(configureBuilderCalled); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); Assert.True(configureBuilderCalled); - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); host.Dispose(); } @@ -469,8 +469,8 @@ public async Task AddOpenTelemetry_HostedServiceOrder_DoesNotMatter() }); var host = builder.Build(); - await host.StartAsync().ConfigureAwait(false); - await host.StopAsync().ConfigureAwait(false); + await host.StartAsync(); + await host.StopAsync(); host.Dispose(); Assert.Single(exportedItems); diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 93ef8753772..a138be0d012 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -23,6 +23,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; using OpenTelemetry.Context.Propagation; @@ -83,7 +84,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values"); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -94,7 +95,7 @@ void ConfigureTestServices(IServiceCollection services) Assert.Single(exportedItems); var activity = exportedItems[0]; - Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); Assert.Equal(ActivityStatusCode.Unset, activity.Status); ValidateAspNetCoreActivity(activity, "/api/values"); } @@ -130,7 +131,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values"); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -177,7 +178,7 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); // Act - var response = await client.SendAsync(request).ConfigureAwait(false); + var response = await client.SendAsync(request); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -189,7 +190,6 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() var activity = exportedItems[0]; Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); - Assert.Equal("api/Values/{id}", activity.DisplayName); Assert.Equal(expectedTraceId, activity.Context.TraceId); Assert.Equal(expectedSpanId, activity.ParentSpanId); @@ -241,7 +241,7 @@ public async Task CustomPropagator(bool addSampler) })) { using var client = testFactory.CreateClient(); - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values/2"); response.EnsureSuccessStatusCode(); // Status Code 200-299 WaitForActivityExport(exportedItems, 1); @@ -251,7 +251,6 @@ public async Task CustomPropagator(bool addSampler) var activity = exportedItems[0]; Assert.True(activity.Duration != TimeSpan.Zero); - Assert.Equal("api/Values/{id}", activity.DisplayName); Assert.Equal(expectedTraceId, activity.Context.TraceId); Assert.Equal(expectedSpanId, activity.ParentSpanId); @@ -292,8 +291,8 @@ void ConfigureTestServices(IServiceCollection services) using var client = testFactory.CreateClient(); // Act - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); // Assert response1.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -344,8 +343,8 @@ void ConfigureTestServices(IServiceCollection services) // Act using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) { - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); response1.EnsureSuccessStatusCode(); // Status Code 200-299 response2.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -389,8 +388,8 @@ public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision // Test TraceContext Propagation var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); - var response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + var response = await client.SendAsync(request); + var childActivityTraceContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); response.EnsureSuccessStatusCode(); @@ -401,8 +400,8 @@ public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision // Test Baggage Context Propagation request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); - response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + response = await client.SendAsync(request); + var childActivityBaggageContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); response.EnsureSuccessStatusCode(); @@ -455,12 +454,12 @@ public async Task ExtractContextIrrespectiveOfTheFilterApplied() // Test TraceContext Propagation var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); - var response = await client.SendAsync(request).ConfigureAwait(false); + var response = await client.SendAsync(request); // Ensure that filter was called Assert.True(isFilterCalled); - var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + var childActivityTraceContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); response.EnsureSuccessStatusCode(); @@ -471,8 +470,8 @@ public async Task ExtractContextIrrespectiveOfTheFilterApplied() // Test Baggage Context Propagation request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); - response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + response = await client.SendAsync(request); + var childActivityBaggageContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); response.EnsureSuccessStatusCode(); @@ -539,7 +538,7 @@ void ConfigureTestServices(IServiceCollection services) request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); + using var response = await client.SendAsync(request); } stopSignal.WaitOne(5000); @@ -593,7 +592,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient(); // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values"); // Assert Assert.Equal(shouldFilterBeCalled, filterCalled); @@ -628,7 +627,7 @@ void ConfigureTestServices(IServiceCollection services) }) .CreateClient()) { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values/2"); response.EnsureSuccessStatusCode(); WaitForActivityExport(exportedItems, 2); } @@ -644,10 +643,9 @@ void ConfigureTestServices(IServiceCollection services) Assert.Equal(activityName, middlewareActivity.OperationName); Assert.Equal(activityName, middlewareActivity.DisplayName); - // tag http.route should be added on activity started by asp.net core - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRoute) as string); + // tag http.method should be added on activity started by asp.net core + Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.DisplayName); } [Theory] @@ -694,7 +692,7 @@ void ConfigureTestServices(IServiceCollection services) try { - using var response = await client.SendAsync(message).ConfigureAwait(false); + using var response = await client.SendAsync(message); response.EnsureSuccessStatusCode(); } catch @@ -747,7 +745,7 @@ public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShould }) .CreateClient()) { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values/2"); response.EnsureSuccessStatusCode(); WaitForActivityExport(exportedItems, 2); } @@ -763,10 +761,9 @@ public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShould Assert.Equal(activityName, middlewareActivity.OperationName); Assert.Equal(activityName, middlewareActivity.DisplayName); - // tag http.route should not be added on activity started by asp.net core as it will not be found during OnEventWritten event - Assert.DoesNotContain(aspnetcoreframeworkactivity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); + // tag http.method should be added on activity started by asp.net core + Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("/api/values/2", aspnetcoreframeworkactivity.DisplayName); } #if NET7_0_OR_GREATER @@ -797,7 +794,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response = await client.GetAsync("/api/values"); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -830,7 +827,7 @@ public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/error").ConfigureAwait(false); + using var response = await client.GetAsync("/api/error"); WaitForActivityExport(exportedItems, 1); } @@ -844,51 +841,42 @@ public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() { int numberOfUnSubscribedEvents = 0; int numberofSubscribedEvents = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } - break; - default: - { - numberOfUnSubscribedEvents++; - } + break; + default: + { + numberOfUnSubscribedEvents++; + } - break; - } - }, - }) - .Build(); - } + break; + } + }, + }) + .Build(); // Arrange using (var client = this.factory .WithWebHostBuilder(builder => { - builder.ConfigureTestServices(ConfigureTestServices); builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); }) .CreateClient()) @@ -896,11 +884,11 @@ void ConfigureTestServices(IServiceCollection services) using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); + using var response = await client.SendAsync(request); } Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(3, numberofSubscribedEvents); + Assert.Equal(2, numberofSubscribedEvents); } [Fact] @@ -909,62 +897,53 @@ public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledExcepti int numberOfUnSubscribedEvents = 0; int numberofSubscribedEvents = 0; int numberOfExceptionCallbacks = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } - break; + break; - // TODO: Add test case for validating name for both the types - // of exception event. - case HttpInListener.OnUnhandledHostingExceptionEvent: - case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: - { - numberofSubscribedEvents++; - numberOfExceptionCallbacks++; - } + // TODO: Add test case for validating name for both the types + // of exception event. + case HttpInListener.OnUnhandledHostingExceptionEvent: + case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: + { + numberofSubscribedEvents++; + numberOfExceptionCallbacks++; + } - break; - default: - { - numberOfUnSubscribedEvents++; - } + break; + default: + { + numberOfUnSubscribedEvents++; + } - break; - } - }, - }) - .Build(); - } + break; + } + }, + }) + .Build(); // Arrange using (var client = this.factory .WithWebHostBuilder(builder => { - builder.ConfigureTestServices(ConfigureTestServices); builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); }) .CreateClient()) @@ -974,7 +953,7 @@ void ConfigureTestServices(IServiceCollection services) using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); + using var response = await client.SendAsync(request); } catch { @@ -984,18 +963,18 @@ void ConfigureTestServices(IServiceCollection services) Assert.Equal(1, numberOfExceptionCallbacks); Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(4, numberofSubscribedEvents); + Assert.Equal(3, numberofSubscribedEvents); } - [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/4884")] + [Fact] public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() { int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; + int numberOfSubscribedEvents = 0; int numberOfExceptionCallbacks = 0; // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation( new TestHttpInListener(new AspNetCoreInstrumentationOptions()) { @@ -1005,13 +984,13 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan { case HttpInListener.OnStartEvent: { - numberofSubscribedEvents++; + numberOfSubscribedEvents++; } break; case HttpInListener.OnStopEvent: { - numberofSubscribedEvents++; + numberOfSubscribedEvents++; } break; @@ -1021,7 +1000,7 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan case HttpInListener.OnUnhandledHostingExceptionEvent: case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: { - numberofSubscribedEvents++; + numberOfSubscribedEvents++; numberOfExceptionCallbacks++; } @@ -1037,96 +1016,35 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan }) .Build(); - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - - app.UseExceptionHandler(handler => - { - handler.Run(async (ctx) => + using (var client = this.factory + .WithWebHostBuilder(builder => { - await ctx.Response.WriteAsync("handled").ConfigureAwait(false); - }); - }); - - app.Map("/error", ThrowException); - - static void ThrowException(IApplicationBuilder app) + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + builder.Configure(app => app + .UseExceptionHandler(handler => + { + handler.Run(async (ctx) => + { + await ctx.Response.WriteAsync("handled"); + }); + })); + }) + .CreateClient()) { - app.Run(context => + try { - throw new Exception("CustomException"); - }); - } - - _ = app.RunAsync(); - - using var client = new HttpClient(); - try - { - await client.GetStringAsync("http://localhost:5000/error").ConfigureAwait(false); - } - catch - { - // ignore 500 error. - } - - Assert.Equal(0, numberOfExceptionCallbacks); - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(2, numberofSubscribedEvents); - - await app.DisposeAsync().ConfigureAwait(false); - } - - [Fact] - public async Task RouteInformationIsNotAddedToRequestsOutsideOfMVC() - { - var exportedItems = new List(); - - // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - - app.MapGet("/custom/{name:alpha}", () => "Hello"); - - _ = app.RunAsync(); - - using var client = new HttpClient(); - var res = await client.GetStringAsync("http://localhost:5000/custom/abc").ConfigureAwait(false); - Assert.NotNull(res); - - tracerprovider.ForceFlush(); - for (var i = 0; i < 10; i++) - { - if (exportedItems.Count > 0) + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + using var response = await client.SendAsync(request); + } + catch { - break; + // ignore exception } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } - var activity = exportedItems[0]; - - Assert.NotNull(activity); - - // After fix update to Contains http.route - Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); - - // After fix this should be /custom/{name:alpha} - Assert.Equal("/custom/abc", activity.DisplayName); - - await app.DisposeAsync().ConfigureAwait(false); + Assert.Equal(0, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberOfSubscribedEvents); } public void Dispose() @@ -1158,7 +1076,7 @@ private static void ValidateAspNetCoreActivity(Activity activityToValidate, stri Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); #endif - Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeHttpTarget) as string); + Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string); } private static void AssertException(List exportedItems) @@ -1195,16 +1113,10 @@ private void ConfigureExceptionFilters(IServiceCollection services, int mode, re .Build(); } - private class ExtractOnlyPropagator : TextMapPropagator + private class ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) : TextMapPropagator { - private readonly ActivityContext activityContext; - private readonly Baggage baggage; - - public ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) - { - this.activityContext = activityContext; - this.baggage = baggage; - } + private readonly ActivityContext activityContext = activityContext; + private readonly Baggage baggage = baggage; public override ISet Fields => throw new NotImplementedException(); @@ -1219,16 +1131,10 @@ public override void Inject(PropagationContext context, T carrier, Action> attributes = null) : Sampler { - private readonly SamplingDecision samplingDecision; - private readonly IEnumerable> attributes; - - public TestSampler(SamplingDecision samplingDecision, IEnumerable> attributes = null) - { - this.samplingDecision = samplingDecision; - this.attributes = attributes; - } + private readonly SamplingDecision samplingDecision = samplingDecision; + private readonly IEnumerable> attributes = attributes; public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { @@ -1236,15 +1142,10 @@ public override SamplingResult ShouldSample(in SamplingParameters samplingParame } } - private class TestHttpInListener : HttpInListener + private class TestHttpInListener(AspNetCoreInstrumentationOptions options) : HttpInListener(options) { public Action OnEventWrittenCallback; - public TestHttpInListener(AspNetCoreInstrumentationOptions options) - : base(options) - { - } - public override void OnEventWritten(string name, object payload) { base.OnEventWritten(name, payload); @@ -1253,17 +1154,11 @@ public override void OnEventWritten(string name, object payload) } } - private class TestNullHostActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl { - private ActivitySource activitySource; + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; private Activity activity; - private string activityName; - - public TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) - { - this.activitySource = new ActivitySource(activitySourceName); - this.activityName = activityName; - } public override void PreProcess(HttpContext context) { @@ -1281,17 +1176,11 @@ public override void PostProcess(HttpContext context) } } - private class TestActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + private class TestActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl { - private ActivitySource activitySource; + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; private Activity activity; - private string activityName; - - public TestActivityMiddlewareImpl(string activitySourceName, string activityName) - { - this.activitySource = new ActivitySource(activitySourceName); - this.activityName = activityName; - } public override void PreProcess(HttpContext context) { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index 1059bee9281..c685f0e2737 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -39,11 +39,11 @@ public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactor } [Theory] - [InlineData("/api/values", null, "user-agent", 503, "503")] - [InlineData("/api/values", "?query=1", null, 503, null)] + [InlineData("/api/values", null, "user-agent", 200, null)] + [InlineData("/api/values", "?query=1", null, 200, null)] [InlineData("/api/exception", null, null, 503, null)] [InlineData("/api/exception", null, null, 503, null, true)] - public async Task SuccessfulTemplateControllerCallGeneratesASpan_Old( + public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( string urlPath, string query, string userAgent, @@ -51,102 +51,94 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_Old( string reasonPhrase, bool recordException = false) { - try - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "none"); - - var exportedItems = new List(); + var exportedItems = new List(); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try + builder.ConfigureTestServices((IServiceCollection services) => { - if (!string.IsNullOrEmpty(userAgent)) - { - client.DefaultRequestHeaders.Add("User-Agent", userAgent); - } - - // Act - var path = urlPath; - if (query != null) - { - path += query; - } - - using var response = await client.GetAsync(path).ConfigureAwait(false); - } - catch (Exception) + services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) + .AddInMemoryExporter(exportedItems)); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + if (!string.IsNullOrEmpty(userAgent)) { - // ignore errors + client.DefaultRequestHeaders.Add("User-Agent", userAgent); } - for (var i = 0; i < 10; i++) + // Act + var path = urlPath; + if (query != null) { - if (exportedItems.Count == 1) - { - break; - } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + path += query; } - } - - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor)); - Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeHttpScheme)); - Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); - Assert.Equal($"http://localhost{urlPath}{query}", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - if (statusCode == 503) - { - Assert.Equal(ActivityStatusCode.Error, activity.Status); + using var response = await client.GetAsync(path); } - else + catch (Exception) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + // ignore errors } - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - Assert.Null(activity.StatusDescription); - - if (recordException) + for (var i = 0; i < 10; i++) { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); + if (exportedItems.Count == 1) + { + break; + } + + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + await Task.Delay(TimeSpan.FromSeconds(1)); } + } - ValidateTagValue(activity, SemanticConventions.AttributeHttpUserAgent, userAgent); + Assert.Single(exportedItems); + var activity = exportedItems[0]; - activity.Dispose(); + Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); + Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); + Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath)); + Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery)); + Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); + + if (statusCode == 503) + { + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType)); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - finally + + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + Assert.Null(activity.StatusDescription); + + if (recordException) { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + Assert.Single(activity.Events); + Assert.Equal("exception", activity.Events.First().Name); } + + ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); + + activity.Dispose(); } private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) @@ -176,7 +168,7 @@ public override async Task ProcessAsync(HttpContext context) { context.Response.StatusCode = this.statusCode; context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); + await context.Response.WriteAsync("empty"); if (context.Request.Path.Value.EndsWith("exception")) { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs deleted file mode 100644 index 8bfe675ed5d..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs +++ /dev/null @@ -1,196 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Trace; -using TestApp.AspNetCore; -using Xunit; - -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; - -public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe - : IClassFixture> -{ - private readonly WebApplicationFactory factory; - - public IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe(WebApplicationFactory factory) - { - this.factory = factory; - } - - [Theory] - [InlineData("/api/values", null, "user-agent", 503, "503")] - [InlineData("/api/values", "?query=1", null, 503, null)] - [InlineData("/api/exception", null, null, 503, null)] - [InlineData("/api/exception", null, null, 503, null, true)] - public async Task SuccessfulTemplateControllerCallGeneratesASpan_Dupe( - string urlPath, - string query, - string userAgent, - int statusCode, - string reasonPhrase, - bool recordException = false) - { - try - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup"); - - var exportedItems = new List(); - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - try - { - if (!string.IsNullOrEmpty(userAgent)) - { - client.DefaultRequestHeaders.Add("User-Agent", userAgent); - } - - // Act - var path = urlPath; - if (query != null) - { - path += query; - } - - using var response = await client.GetAsync(path).ConfigureAwait(false); - } - catch (Exception) - { - // ignore errors - } - - for (var i = 0; i < 10; i++) - { - if (exportedItems.Count == 1) - { - break; - } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); - } - } - - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor)); - Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); - Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeHttpScheme)); - Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath)); - Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); - Assert.Equal($"http://localhost{urlPath}{query}", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); - Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery)); - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - - if (statusCode == 503) - { - Assert.Equal(ActivityStatusCode.Error, activity.Status); - } - else - { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - } - - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - Assert.Null(activity.StatusDescription); - - if (recordException) - { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); - } - - ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); - - activity.Dispose(); - } - finally - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); - } - } - - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) - { - if (string.IsNullOrEmpty(expectedValue)) - { - Assert.Null(activity.GetTagValue(attribute)); - } - else - { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); - } - } - - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl - { - private readonly int statusCode; - private readonly string reasonPhrase; - - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } - - public override async Task ProcessAsync(HttpContext context) - { - context.Response.StatusCode = this.statusCode; - context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); - - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } - - return false; - } - } -} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs deleted file mode 100644 index 5c56ffb6719..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs +++ /dev/null @@ -1,190 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Trace; -using TestApp.AspNetCore; -using Xunit; - -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; - -public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_New - : IClassFixture> -{ - private readonly WebApplicationFactory factory; - - public IncomingRequestsCollectionsIsAccordingToTheSpecTests_New(WebApplicationFactory factory) - { - this.factory = factory; - } - - [Theory] - [InlineData("/api/values", null, "user-agent", 200, null)] - [InlineData("/api/values", "?query=1", null, 200, null)] - [InlineData("/api/exception", null, null, 503, null)] - [InlineData("/api/exception", null, null, 503, null, true)] - public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( - string urlPath, - string query, - string userAgent, - int statusCode, - string reasonPhrase, - bool recordException = false) - { - try - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http"); - - var exportedItems = new List(); - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - try - { - if (!string.IsNullOrEmpty(userAgent)) - { - client.DefaultRequestHeaders.Add("User-Agent", userAgent); - } - - // Act - var path = urlPath; - if (query != null) - { - path += query; - } - - using var response = await client.GetAsync(path).ConfigureAwait(false); - } - catch (Exception) - { - // ignore errors - } - - for (var i = 0; i < 10; i++) - { - if (exportedItems.Count == 1) - { - break; - } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); - } - } - - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); - Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); - Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath)); - Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery)); - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); - - if (statusCode == 503) - { - Assert.Equal(ActivityStatusCode.Error, activity.Status); - Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType)); - } - else - { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - } - - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - Assert.Null(activity.StatusDescription); - - if (recordException) - { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); - } - - ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); - - activity.Dispose(); - } - finally - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); - } - } - - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) - { - if (string.IsNullOrEmpty(expectedValue)) - { - Assert.Null(activity.GetTagValue(attribute)); - } - else - { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); - } - } - - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl - { - private readonly int statusCode; - private readonly string reasonPhrase; - - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } - - public override async Task ProcessAsync(HttpContext context) - { - context.Response.StatusCode = this.statusCode; - context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); - - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } - - return false; - } - } -} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 0185df65271..b5769f4cb48 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -26,8 +26,10 @@ #if NET8_0_OR_GREATER using Microsoft.AspNetCore.RateLimiting; #endif -using Microsoft.Extensions.Configuration; +#if NET8_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +#endif using Microsoft.Extensions.Logging; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -35,72 +37,67 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; -public class MetricTests - : IClassFixture>, IDisposable +public class MetricTests(WebApplicationFactory factory) + : IClassFixture>, IDisposable { - public const string SemanticConventionOptInKeyName = "OTEL_SEMCONV_STABILITY_OPT_IN"; - private const int StandardTagsCount = 6; - private readonly WebApplicationFactory factory; + private readonly WebApplicationFactory factory = factory; private MeterProvider meterProvider; - public MetricTests(WebApplicationFactory factory) - { - this.factory = factory; - } - [Fact] public void AddAspNetCoreInstrumentation_BadArgs() { MeterProviderBuilder builder = null; - Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + Assert.Throws(builder.AddAspNetCoreInstrumentation); } #if NET8_0_OR_GREATER [Fact] public async Task ValidateNet8MetricsAsync() { - var metricItems = new List(); - + var exportedItems = new List(); this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) - .Build(); + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); + builder.WebHost.UseUrls("http://*:0"); var app = builder.Build(); app.MapGet("/", () => "Hello"); _ = app.RunAsync(); + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); + using var client = new HttpClient(); - var res = await client.GetStringAsync("http://localhost:5000/").ConfigureAwait(false); - Assert.NotNull(res); + var res = await client.GetAsync($"http://localhost:{portNumber}/"); + Assert.True(res.IsSuccessStatusCode); // We need to let metric callback execute as it is executed AFTER response was returned. // In unit tests environment there may be a lot of parallel unit tests executed, so // giving some breezing room for the callbacks to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)); this.meterProvider.Dispose(); - var requestDurationMetric = metricItems + var requestDurationMetric = exportedItems .Count(item => item.Name == "http.server.request.duration"); - var activeRequestsMetric = metricItems. + var activeRequestsMetric = exportedItems. Count(item => item.Name == "http.server.active_requests"); - var routeMatchingMetric = metricItems. + var routeMatchingMetric = exportedItems. Count(item => item.Name == "aspnetcore.routing.match_attempts"); - var kestrelActiveConnectionsMetric = metricItems. - Count(item => item.Name == "kestrel.active_connections"); + var kestrelActiveConnectionsMetric = exportedItems. + Count(item => item.Name == "kestrel.active_connections"); - var kestrelQueuedConnectionMetric = metricItems. - Count(item => item.Name == "kestrel.queued_connections"); + var kestrelQueuedConnectionMetric = exportedItems. + Count(item => item.Name == "kestrel.queued_connections"); Assert.Equal(1, requestDurationMetric); Assert.Equal(1, activeRequestsMetric); @@ -121,22 +118,28 @@ public async Task ValidateNet8MetricsAsync() [Fact] public async Task ValidateNet8RateLimitingMetricsAsync() { - var metricItems = new List(); + var exportedItems = new List(); - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) - .Build(); + void ConfigureTestServices(IServiceCollection services) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + services.AddRateLimiter(_ => _ + .AddFixedWindowLimiter(policyName: "fixed", options => + { + options.PermitLimit = 4; + options.Window = TimeSpan.FromSeconds(12); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 2; + })); + } var builder = WebApplication.CreateBuilder(); - builder.Services.AddRateLimiter(_ => _ - .AddFixedWindowLimiter(policyName: "fixed", options => - { - options.PermitLimit = 4; - options.Window = TimeSpan.FromSeconds(12); - options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; - options.QueueLimit = 2; - })); + builder.WebHost.UseUrls("http://*:0"); + ConfigureTestServices(builder.Services); builder.Logging.ClearProviders(); var app = builder.Build(); @@ -150,30 +153,33 @@ public async Task ValidateNet8RateLimitingMetricsAsync() _ = app.RunAsync(); + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); + using var client = new HttpClient(); - var res = await client.GetStringAsync("http://localhost:5000/").ConfigureAwait(false); + var res = await client.GetAsync($"http://localhost:{portNumber}/"); Assert.NotNull(res); // We need to let metric callback execute as it is executed AFTER response was returned. // In unit tests environment there may be a lot of parallel unit tests executed, so // giving some breezing room for the callbacks to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)); this.meterProvider.Dispose(); - var activeRequestleasesMetric = metricItems + var activeRequestLeasesMetric = exportedItems .Where(item => item.Name == "aspnetcore.rate_limiting.active_request_leases") .ToArray(); - var requestLeaseDurationMetric = metricItems. + var requestLeaseDurationMetric = exportedItems. Where(item => item.Name == "aspnetcore.rate_limiting.request_lease.duration") .ToArray(); - var limitingRequestsMetric = metricItems. + var limitingRequestsMetric = exportedItems. Where(item => item.Name == "aspnetcore.rate_limiting.requests") .ToArray(); - Assert.Single(activeRequestleasesMetric); + Assert.Single(activeRequestLeasesMetric); Assert.Single(requestLeaseDurationMetric); Assert.Single(limitingRequestsMetric); @@ -188,16 +194,11 @@ public async Task ValidateNet8RateLimitingMetricsAsync() [Theory] [InlineData("/api/values/2", "api/Values/{id}", null, 200)] [InlineData("/api/Error", "api/Error", "System.Exception", 500)] - public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode) + public async Task RequestMetricIsCaptured(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode) { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) - .Build(); - var metricItems = new List(); this.meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddAspNetCoreInstrumentation() .AddInMemoryExporter(metricItems) .Build(); @@ -211,7 +212,7 @@ public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, { try { - using var response = await client.GetAsync(api).ConfigureAwait(false); + using var response = await client.GetAsync(api); response.EnsureSuccessStatusCode(); } catch @@ -223,7 +224,7 @@ public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, // We need to let End callback execute as it is executed AFTER response was returned. // In unit tests environment there may be a lot of parallel unit tests executed, so // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)); this.meterProvider.Dispose(); @@ -237,12 +238,12 @@ public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, var metricPoints = GetMetricPoints(metric); Assert.Single(metricPoints); - AssertMetricPoints_New( + AssertMetricPoints( metricPoints: metricPoints, expectedRoutes: new List { expectedRoute }, expectedErrorType, expectedStatusCode, - expectedTagsCount: expectedErrorType == null ? 6 : 7); + expectedTagsCount: expectedErrorType == null ? 5 : 6); } [Theory] @@ -259,14 +260,9 @@ public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, [InlineData("CUSTOM", "_OTHER")] public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, string expectedMethod) { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) - .Build(); - var metricItems = new List(); this.meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddAspNetCoreInstrumentation() .AddInMemoryExporter(metricItems) .Build(); @@ -283,7 +279,7 @@ public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, st try { - using var response = await client.SendAsync(message).ConfigureAwait(false); + using var response = await client.SendAsync(message); } catch { @@ -293,7 +289,7 @@ public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, st // We need to let End callback execute as it is executed AFTER response was returned. // In unit tests environment there may be a lot of parallel unit tests executed, so // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)); this.meterProvider.Dispose(); @@ -321,129 +317,6 @@ public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, st Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); } -#if !NET8_0_OR_GREATER - [Fact] - public async Task RequestMetricIsCaptured_Old() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = null }) - .Build(); - - var metricItems = new List(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) - .Build(); - - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - - response1.EnsureSuccessStatusCode(); - response2.EnsureSuccessStatusCode(); - } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); - - this.meterProvider.Dispose(); - - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); - - var metric = Assert.Single(requestMetrics); - Assert.Equal("ms", metric.Unit); - var metricPoints = GetMetricPoints(metric); - Assert.Equal(2, metricPoints.Count); - - AssertMetricPoints_Old( - metricPoints: metricPoints, - expectedRoutes: new List { "api/Values", "api/Values/{id}" }, - expectedTagsCount: 6); - } - - [Fact] - public async Task RequestMetricIsCaptured_Dup() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http/dup" }) - .Build(); - - var metricItems = new List(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) - .Build(); - - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - - response1.EnsureSuccessStatusCode(); - response2.EnsureSuccessStatusCode(); - } - - // We need to let End callback execute as it is executed AFTER response was returned. - // In unit tests environment there may be a lot of parallel unit tests executed, so - // giving some breezing room for the End callback to complete - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); - - this.meterProvider.Dispose(); - - // Validate Old Semantic Convention - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); - - var metric = Assert.Single(requestMetrics); - Assert.Equal("ms", metric.Unit); - var metricPoints = GetMetricPoints(metric); - Assert.Equal(2, metricPoints.Count); - - AssertMetricPoints_Old( - metricPoints: metricPoints, - expectedRoutes: new List { "api/Values", "api/Values/{id}" }, - expectedTagsCount: 6); - - // Validate New Semantic Convention - requestMetrics = metricItems - .Where(item => item.Name == "http.server.request.duration") - .ToArray(); - - metric = Assert.Single(requestMetrics); - - Assert.Equal("s", metric.Unit); - metricPoints = GetMetricPoints(metric); - Assert.Equal(2, metricPoints.Count); - - AssertMetricPoints_New( - metricPoints: metricPoints, - expectedRoutes: new List { "api/Values", "api/Values/{id}" }, - null, - 200, - expectedTagsCount: 6); - } -#endif - public void Dispose() { this.meterProvider?.Dispose(); @@ -463,7 +336,7 @@ private static List GetMetricPoints(Metric metric) return metricPoints; } - private static void AssertMetricPoints_New( + private static void AssertMetricPoints( List metricPoints, List expectedRoutes, string expectedErrorType, @@ -488,39 +361,7 @@ private static void AssertMetricPoints_New( if (metricPoint.HasValue) { - AssertMetricPoint_New(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount); - } - else - { - Assert.Fail($"A metric for route '{expectedRoute}' was not found"); - } - } - } - - private static void AssertMetricPoints_Old( - List metricPoints, - List expectedRoutes, - int expectedTagsCount) - { - // Assert that one MetricPoint exists for each ExpectedRoute - foreach (var expectedRoute in expectedRoutes) - { - MetricPoint? metricPoint = null; - - foreach (var mp in metricPoints) - { - foreach (var tag in mp.Tags) - { - if (tag.Key == SemanticConventions.AttributeHttpRoute && tag.Value.ToString() == expectedRoute) - { - metricPoint = mp; - } - } - } - - if (metricPoint.HasValue) - { - AssertMetricPoint_Old(metricPoint.Value, expectedRoute, expectedTagsCount); + AssertMetricPoint(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount); } else { @@ -529,7 +370,7 @@ private static void AssertMetricPoints_Old( } } - private static KeyValuePair[] AssertMetricPoint_New( + private static void AssertMetricPoint( MetricPoint metricPoint, int expectedStatusCode, string expectedRoute, @@ -565,13 +406,8 @@ private static KeyValuePair[] AssertMetricPoint_New( if (expectedErrorType != null) { -#if NET8_0_OR_GREATER - // Expected to change in next release - // https://github.com/dotnet/aspnetcore/issues/51029 - var errorType = new KeyValuePair("exception.type", expectedErrorType); -#else var errorType = new KeyValuePair(SemanticConventions.AttributeErrorType, expectedErrorType); -#endif + Assert.Contains(errorType, attributes); } @@ -592,56 +428,5 @@ private static KeyValuePair[] AssertMetricPoint_New( Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds); Assert.True(histogramBoundsMatchCorrectly); - - return attributes; - } - - private static KeyValuePair[] AssertMetricPoint_Old( - MetricPoint metricPoint, - string expectedRoute = "api/Values", - int expectedTagsCount = StandardTagsCount) - { - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); - - Assert.Equal(1L, count); - Assert.True(sum > 0); - - var attributes = new KeyValuePair[metricPoint.Tags.Count]; - int i = 0; - foreach (var tag in metricPoint.Tags) - { - attributes[i++] = tag; - } - - // Inspect Attributes - Assert.Equal(expectedTagsCount, attributes.Length); - - var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, "GET"); - var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); - var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, 200); - var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "1.1"); - var host = new KeyValuePair(SemanticConventions.AttributeNetHostName, "localhost"); - var route = new KeyValuePair(SemanticConventions.AttributeHttpRoute, expectedRoute); - Assert.Contains(method, attributes); - Assert.Contains(scheme, attributes); - Assert.Contains(statusCode, attributes); - Assert.Contains(flavor, attributes); - Assert.Contains(host, attributes); - Assert.Contains(route, attributes); - - // Inspect Histogram Bounds - var histogramBuckets = metricPoint.GetHistogramBuckets(); - var histogramBounds = new List(); - foreach (var t in histogramBuckets) - { - histogramBounds.Add(t.ExplicitBound); - } - - Assert.Equal( - expected: new List { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000, double.PositiveInfinity }, - actual: histogramBounds); - - return attributes; } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md index ac9ff0d5117..c12dc40dadc 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md @@ -1,35 +1,35 @@ # Test results for ASP.NET Core 6 -| Span http.route | Metric http.route | App | Test Name | -| - | - | - | - | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | -| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | -| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | -| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | -| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | -| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | -| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | -| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) | -| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | -| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | ## ConventionalRouting: Root path ```json { "IdealHttpRoute": "ConventionalRoute/Default/{id?}", - "ActivityDisplayName": "/", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -58,8 +58,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -92,8 +92,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -125,7 +125,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/ConventionalRoute/NotFound", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -144,8 +144,8 @@ ```json { "IdealHttpRoute": "SomePath/{id}/{num:int}", - "ActivityDisplayName": "/SomePath/SomeString/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", "MetricHttpRoute": "SomePath/{id}/{num:int}", "RouteInfo": { "HttpMethod": "GET", @@ -179,7 +179,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/SomePath/SomeString/NotAnInt", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -198,8 +198,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", - "ActivityDisplayName": "/MyArea", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -229,8 +229,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", - "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -260,8 +260,8 @@ ```json { "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", - "ActivityDisplayName": "/SomePrefix", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -291,7 +291,7 @@ ```json { "IdealHttpRoute": "AttributeRoute", - "ActivityDisplayName": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", "ActivityHttpRoute": "AttributeRoute", "MetricHttpRoute": "AttributeRoute", "RouteInfo": { @@ -321,7 +321,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get", - "ActivityDisplayName": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", "ActivityHttpRoute": "AttributeRoute/Get", "MetricHttpRoute": "AttributeRoute/Get", "RouteInfo": { @@ -351,7 +351,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get/{id}", - "ActivityDisplayName": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", "ActivityHttpRoute": "AttributeRoute/Get/{id}", "MetricHttpRoute": "AttributeRoute/Get/{id}", "RouteInfo": { @@ -384,7 +384,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -417,7 +417,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -450,7 +450,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "/", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -479,7 +479,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "Index", + "ActivityDisplayName": "GET Index", "ActivityHttpRoute": "Index", "MetricHttpRoute": "Index", "RouteInfo": { @@ -508,7 +508,7 @@ ```json { "IdealHttpRoute": "/PageThatThrowsException", - "ActivityDisplayName": "PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", "ActivityHttpRoute": "PageThatThrowsException", "MetricHttpRoute": "PageThatThrowsException", "RouteInfo": { @@ -537,7 +537,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/js/site.js", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -556,8 +556,8 @@ ```json { "IdealHttpRoute": "/MinimalApi", - "ActivityDisplayName": "/MinimalApi", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", "MetricHttpRoute": "/MinimalApi", "RouteInfo": { "HttpMethod": "GET", @@ -575,8 +575,8 @@ ```json { "IdealHttpRoute": "/MinimalApi/{id}", - "ActivityDisplayName": "/MinimalApi/123", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", "MetricHttpRoute": "/MinimalApi/{id}", "RouteInfo": { "HttpMethod": "GET", diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md index f93e5de8ea8..ec252654a2a 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md @@ -1,37 +1,37 @@ # Test results for ASP.NET Core 7 -| Span http.route | Metric http.route | App | Test Name | -| - | - | - | - | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | -| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | -| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | -| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | -| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | -| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | -| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | -| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) | -| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | -| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | -| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | -| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | +| :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | ## ConventionalRouting: Root path ```json { "IdealHttpRoute": "ConventionalRoute/Default/{id?}", - "ActivityDisplayName": "/", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -60,8 +60,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -94,8 +94,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -127,7 +127,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/ConventionalRoute/NotFound", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -146,8 +146,8 @@ ```json { "IdealHttpRoute": "SomePath/{id}/{num:int}", - "ActivityDisplayName": "/SomePath/SomeString/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", "MetricHttpRoute": "SomePath/{id}/{num:int}", "RouteInfo": { "HttpMethod": "GET", @@ -181,7 +181,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/SomePath/SomeString/NotAnInt", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -200,8 +200,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", - "ActivityDisplayName": "/MyArea", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -231,8 +231,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", - "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -262,8 +262,8 @@ ```json { "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", - "ActivityDisplayName": "/SomePrefix", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -293,7 +293,7 @@ ```json { "IdealHttpRoute": "AttributeRoute", - "ActivityDisplayName": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", "ActivityHttpRoute": "AttributeRoute", "MetricHttpRoute": "AttributeRoute", "RouteInfo": { @@ -323,7 +323,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get", - "ActivityDisplayName": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", "ActivityHttpRoute": "AttributeRoute/Get", "MetricHttpRoute": "AttributeRoute/Get", "RouteInfo": { @@ -353,7 +353,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get/{id}", - "ActivityDisplayName": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", "ActivityHttpRoute": "AttributeRoute/Get/{id}", "MetricHttpRoute": "AttributeRoute/Get/{id}", "RouteInfo": { @@ -386,7 +386,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -419,7 +419,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -452,7 +452,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "/", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -481,7 +481,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "Index", + "ActivityDisplayName": "GET Index", "ActivityHttpRoute": "Index", "MetricHttpRoute": "Index", "RouteInfo": { @@ -510,7 +510,7 @@ ```json { "IdealHttpRoute": "/PageThatThrowsException", - "ActivityDisplayName": "PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", "ActivityHttpRoute": "PageThatThrowsException", "MetricHttpRoute": "PageThatThrowsException", "RouteInfo": { @@ -539,7 +539,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/js/site.js", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -558,8 +558,8 @@ ```json { "IdealHttpRoute": "/MinimalApi", - "ActivityDisplayName": "/MinimalApi", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", "MetricHttpRoute": "/MinimalApi", "RouteInfo": { "HttpMethod": "GET", @@ -577,8 +577,8 @@ ```json { "IdealHttpRoute": "/MinimalApi/{id}", - "ActivityDisplayName": "/MinimalApi/123", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", "MetricHttpRoute": "/MinimalApi/{id}", "RouteInfo": { "HttpMethod": "GET", @@ -598,8 +598,8 @@ ```json { "IdealHttpRoute": "/MinimalApiUsingMapGroup/", - "ActivityDisplayName": "/MinimalApiUsingMapGroup", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", "MetricHttpRoute": "/MinimalApiUsingMapGroup/", "RouteInfo": { "HttpMethod": "GET", @@ -617,8 +617,8 @@ ```json { "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}", - "ActivityDisplayName": "/MinimalApiUsingMapGroup/123", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", "RouteInfo": { "HttpMethod": "GET", diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md index 4cacc1eac0c..3b712f73e20 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md @@ -1,37 +1,37 @@ # Test results for ASP.NET Core 8 -| Span http.route | Metric http.route | App | Test Name | -| - | - | - | - | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | -| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | -| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | -| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | -| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | -| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | -| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | -| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | -| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | -| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | -| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) | -| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | -| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | -| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | -| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | +| :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | ## ConventionalRouting: Root path ```json { "IdealHttpRoute": "ConventionalRoute/Default/{id?}", - "ActivityDisplayName": "/", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -60,8 +60,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -94,8 +94,8 @@ ```json { "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", - "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -127,7 +127,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/ConventionalRoute/NotFound", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -146,8 +146,8 @@ ```json { "IdealHttpRoute": "SomePath/{id}/{num:int}", - "ActivityDisplayName": "/SomePath/SomeString/2", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", "MetricHttpRoute": "SomePath/{id}/{num:int}", "RouteInfo": { "HttpMethod": "GET", @@ -181,7 +181,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/SomePath/SomeString/NotAnInt", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -200,8 +200,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", - "ActivityDisplayName": "/MyArea", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -231,8 +231,8 @@ ```json { "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", - "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -262,8 +262,8 @@ ```json { "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", - "ActivityDisplayName": "/SomePrefix", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "RouteInfo": { "HttpMethod": "GET", @@ -293,7 +293,7 @@ ```json { "IdealHttpRoute": "AttributeRoute", - "ActivityDisplayName": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", "ActivityHttpRoute": "AttributeRoute", "MetricHttpRoute": "AttributeRoute", "RouteInfo": { @@ -323,7 +323,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get", - "ActivityDisplayName": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", "ActivityHttpRoute": "AttributeRoute/Get", "MetricHttpRoute": "AttributeRoute/Get", "RouteInfo": { @@ -353,7 +353,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/Get/{id}", - "ActivityDisplayName": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", "ActivityHttpRoute": "AttributeRoute/Get/{id}", "MetricHttpRoute": "AttributeRoute/Get/{id}", "RouteInfo": { @@ -386,7 +386,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -419,7 +419,7 @@ ```json { "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", "RouteInfo": { @@ -452,7 +452,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "/", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -481,7 +481,7 @@ ```json { "IdealHttpRoute": "/Index", - "ActivityDisplayName": "Index", + "ActivityDisplayName": "GET Index", "ActivityHttpRoute": "Index", "MetricHttpRoute": "Index", "RouteInfo": { @@ -510,7 +510,7 @@ ```json { "IdealHttpRoute": "/PageThatThrowsException", - "ActivityDisplayName": "PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", "ActivityHttpRoute": "PageThatThrowsException", "MetricHttpRoute": "PageThatThrowsException", "RouteInfo": { @@ -539,7 +539,7 @@ ```json { "IdealHttpRoute": "", - "ActivityDisplayName": "/js/site.js", + "ActivityDisplayName": "GET", "ActivityHttpRoute": "", "MetricHttpRoute": "", "RouteInfo": { @@ -558,8 +558,8 @@ ```json { "IdealHttpRoute": "/MinimalApi", - "ActivityDisplayName": "/MinimalApi", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", "MetricHttpRoute": "/MinimalApi", "RouteInfo": { "HttpMethod": "GET", @@ -577,8 +577,8 @@ ```json { "IdealHttpRoute": "/MinimalApi/{id}", - "ActivityDisplayName": "/MinimalApi/123", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", "MetricHttpRoute": "/MinimalApi/{id}", "RouteInfo": { "HttpMethod": "GET", @@ -598,8 +598,8 @@ ```json { "IdealHttpRoute": "/MinimalApiUsingMapGroup/", - "ActivityDisplayName": "/MinimalApiUsingMapGroup", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", "MetricHttpRoute": "/MinimalApiUsingMapGroup/", "RouteInfo": { "HttpMethod": "GET", @@ -617,8 +617,8 @@ ```json { "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}", - "ActivityDisplayName": "/MinimalApiUsingMapGroup/123", - "ActivityHttpRoute": "", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", "RouteInfo": { "HttpMethod": "GET", diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs index d2b7bb730df..bf5c40cbcb1 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -49,8 +49,7 @@ private static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable< continue; } - result.Add(new object[] { testCase, true }); - result.Add(new object[] { testCase, false }); + result.Add(new object[] { testCase }); } return result; @@ -72,11 +71,7 @@ public class TestCase public string? ExpectedHttpRoute { get; set; } - public string? CurrentActivityDisplayName { get; set; } - - public string? CurrentActivityHttpRoute { get; set; } - - public string? CurrentMetricHttpRoute { get; set; } + public string? CurrentHttpRoute { get; set; } public override string ToString() { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json index c5c8febb77b..4d871d986a1 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json @@ -5,9 +5,7 @@ "httpMethod": "GET", "path": "/", "expectedStatusCode": 200, - "currentActivityDisplayName": "/", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "expectedHttpRoute": "ConventionalRoute/Default/{id?}" }, { @@ -16,9 +14,7 @@ "httpMethod": "GET", "path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", "expectedStatusCode": 200, - "currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" }, { @@ -27,9 +23,7 @@ "httpMethod": "GET", "path": "/ConventionalRoute/ActionWithStringParameter?num=3", "expectedStatusCode": 200, - "currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" }, { @@ -38,9 +32,7 @@ "httpMethod": "GET", "path": "/ConventionalRoute/NotFound", "expectedStatusCode": 404, - "currentActivityDisplayName": "/ConventionalRoute/NotFound", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "", + "currentHttpRoute": null, "expectedHttpRoute": "" }, { @@ -49,9 +41,7 @@ "httpMethod": "GET", "path": "/SomePath/SomeString/2", "expectedStatusCode": 200, - "currentActivityDisplayName": "/SomePath/SomeString/2", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "SomePath/{id}/{num:int}" }, { @@ -60,9 +50,7 @@ "httpMethod": "GET", "path": "/SomePath/SomeString/NotAnInt", "expectedStatusCode": 404, - "currentActivityDisplayName": "/SomePath/SomeString/NotAnInt", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "" }, { @@ -71,9 +59,7 @@ "httpMethod": "GET", "path": "/MyArea", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MyArea", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "currentHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "expectedHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}" }, { @@ -82,9 +68,7 @@ "httpMethod": "GET", "path": "/MyArea/ControllerForMyArea/NonDefault", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "currentHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", "expectedHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}" }, { @@ -93,9 +77,7 @@ "httpMethod": "GET", "path": "/SomePrefix", "expectedStatusCode": 200, - "currentActivityDisplayName": "/SomePrefix", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "currentHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", "expectedHttpRoute": "SomePrefix/AnotherArea/Index/{id?}" }, { @@ -104,9 +86,7 @@ "httpMethod": "GET", "path": "/AttributeRoute", "expectedStatusCode": 200, - "currentActivityDisplayName": "AttributeRoute", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "AttributeRoute" }, { @@ -115,9 +95,7 @@ "httpMethod": "GET", "path": "/AttributeRoute/Get", "expectedStatusCode": 200, - "currentActivityDisplayName": "AttributeRoute/Get", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "AttributeRoute/Get" }, { @@ -126,9 +104,7 @@ "httpMethod": "GET", "path": "/AttributeRoute/Get/12", "expectedStatusCode": 200, - "currentActivityDisplayName": "AttributeRoute/Get/{id}", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "AttributeRoute/Get/{id}" }, { @@ -137,9 +113,7 @@ "httpMethod": "GET", "path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", "expectedStatusCode": 200, - "currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" }, { @@ -148,9 +122,7 @@ "httpMethod": "GET", "path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", "expectedStatusCode": 400, - "currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" }, { @@ -159,9 +131,7 @@ "httpMethod": "GET", "path": "/", "expectedStatusCode": 200, - "currentActivityDisplayName": "/", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": "", + "currentHttpRoute": "", "expectedHttpRoute": "/Index" }, { @@ -170,9 +140,7 @@ "httpMethod": "GET", "path": "/Index", "expectedStatusCode": 200, - "currentActivityDisplayName": "Index", - "currentActivityHttpRoute": "Index", - "currentMetricHttpRoute": "Index", + "currentHttpRoute": "Index", "expectedHttpRoute": "/Index" }, { @@ -181,9 +149,7 @@ "httpMethod": "GET", "path": "/PageThatThrowsException", "expectedStatusCode": 500, - "currentActivityDisplayName": "PageThatThrowsException", - "currentActivityHttpRoute": "PageThatThrowsException", - "currentMetricHttpRoute": "PageThatThrowsException", + "currentHttpRoute": "PageThatThrowsException", "expectedHttpRoute": "/PageThatThrowsException" }, { @@ -192,9 +158,7 @@ "httpMethod": "GET", "path": "/js/site.js", "expectedStatusCode": 200, - "currentActivityDisplayName": "/js/site.js", - "currentActivityHttpRoute": null, - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "" }, { @@ -203,9 +167,7 @@ "httpMethod": "GET", "path": "/MinimalApi", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MinimalApi", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "/MinimalApi" }, { @@ -214,9 +176,7 @@ "httpMethod": "GET", "path": "/MinimalApi/123", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MinimalApi/123", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "/MinimalApi/{id}" }, { @@ -226,9 +186,7 @@ "httpMethod": "GET", "path": "/MinimalApiUsingMapGroup", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MinimalApiUsingMapGroup", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "/MinimalApiUsingMapGroup/" }, { @@ -238,9 +196,7 @@ "httpMethod": "GET", "path": "/MinimalApiUsingMapGroup/123", "expectedStatusCode": 200, - "currentActivityDisplayName": "/MinimalApiUsingMapGroup/123", - "currentActivityHttpRoute": "", - "currentMetricHttpRoute": null, + "currentHttpRoute": null, "expectedHttpRoute": "/MinimalApiUsingMapGroup/{id}" } ] diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs index 1addda01cb6..c05e5b9e4d5 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -51,7 +51,7 @@ public async Task MakeRequest(TestApplicationScenario scenario, string path) var app = this.apps[scenario]; var baseUrl = app.Urls.First(); var url = $"{baseUrl}{path}"; - await HttpClient.GetAsync(url).ConfigureAwait(false); + await HttpClient.GetAsync(url); } public void AddTestResult(RoutingTestResult result) @@ -77,16 +77,14 @@ private void GenerateReadme() var sb = new StringBuilder(); sb.AppendLine($"# Test results for ASP.NET Core {Environment.Version.Major}"); sb.AppendLine(); - sb.AppendLine("| Span http.route | Metric http.route | App | Test Name |"); - sb.AppendLine("| - | - | - | - |"); + sb.AppendLine("| http.route | App | Test Name |"); + sb.AppendLine("| - | - | - |"); for (var i = 0; i < this.testResults.Count; ++i) { var result = this.testResults[i]; - var emoji1 = result.TestCase.CurrentActivityHttpRoute == null ? ":green_heart:" : ":broken_heart:"; - var emoji2 = result.TestCase.CurrentMetricHttpRoute == null ? ":green_heart:" : ":broken_heart:"; - sb.Append($"| {emoji1} | {emoji2} "); - sb.AppendLine($"| {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({MakeAnchorTag(result.TestCase.TestApplicationScenario, result.TestCase.Name)}) |"); + var emoji = result.TestCase.CurrentHttpRoute == null ? ":green_heart:" : ":broken_heart:"; + sb.AppendLine($"| {emoji} | {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({MakeAnchorTag(result.TestCase.TestApplicationScenario, result.TestCase.Name)}) |"); } for (var i = 0; i < this.testResults.Count; ++i) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs index d8983db9d4d..f03b21c0405 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -17,14 +17,11 @@ #nullable enable using System.Diagnostics; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using RouteTests.TestApplication; using Xunit; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace RouteTests; @@ -49,20 +46,14 @@ public RoutingTests(RoutingTestFixture fixture) [Theory] [MemberData(nameof(TestData))] - public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLegacyConventions) + public async Task TestHttpRoute(RoutingTestCases.TestCase testCase) { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = useLegacyConventions ? null : "http" }) - .Build(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddAspNetCoreInstrumentation() .AddInMemoryExporter(this.exportedActivities) .Build()!; using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddAspNetCoreInstrumentation() .AddInMemoryExporter(this.exportedMetrics) .Build()!; @@ -76,7 +67,7 @@ public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLega break; } - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)); } meterProvider.ForceFlush(); @@ -91,73 +82,45 @@ public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLega var activity = Assert.Single(this.exportedActivities); var metricPoint = Assert.Single(metricPoints); - GetTagsFromActivity(useLegacyConventions, activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute); - GetTagsFromMetricPoint(useLegacyConventions && Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute); + GetTagsFromActivity(activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute); + GetTagsFromMetricPoint(Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute); Assert.Equal(testCase.ExpectedStatusCode, activityHttpStatusCode); Assert.Equal(testCase.ExpectedStatusCode, metricHttpStatusCode); Assert.Equal(testCase.HttpMethod, activityHttpMethod); Assert.Equal(testCase.HttpMethod, metricHttpMethod); - // TODO: The CurrentActivityDisplayName, CurrentActivityHttpRoute, and CurrentMetricHttpRoute - // properties will go away. They only serve to capture status quo. The "else" blocks are the real - // asserts that we ultimately want. - // If any of the current properties are null, then that means we already conform to the - // correct behavior. - if (testCase.CurrentActivityDisplayName != null) - { - Assert.Equal(testCase.CurrentActivityDisplayName, activity.DisplayName); - } - else - { - // Activity.DisplayName should be a combination of http.method + http.route attributes, see: - // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name - var expectedActivityDisplayName = string.IsNullOrEmpty(testCase.ExpectedHttpRoute) - ? testCase.HttpMethod - : $"{testCase.HttpMethod} {testCase.ExpectedHttpRoute}"; - - Assert.Equal(expectedActivityDisplayName, activity.DisplayName); - } + // TODO: The CurrentHttpRoute property will go away. It They only serve to capture status quo. + // If CurrentHttpRoute is null, then that means we already conform to the correct behavior. + var expectedHttpRoute = testCase.CurrentHttpRoute != null ? testCase.CurrentHttpRoute : testCase.ExpectedHttpRoute; + Assert.Equal(expectedHttpRoute, activityHttpRoute); + Assert.Equal(expectedHttpRoute, metricHttpRoute); - if (testCase.CurrentActivityHttpRoute != null) - { - Assert.Equal(testCase.CurrentActivityHttpRoute, activityHttpRoute); - } - else - { - Assert.Equal(testCase.ExpectedHttpRoute, activityHttpRoute); - } + // Activity.DisplayName should be a combination of http.method + http.route attributes, see: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name + var expectedActivityDisplayName = string.IsNullOrEmpty(expectedHttpRoute) + ? testCase.HttpMethod + : $"{testCase.HttpMethod} {expectedHttpRoute}"; - if (testCase.CurrentMetricHttpRoute != null) - { - Assert.Equal(testCase.CurrentMetricHttpRoute, metricHttpRoute); - } - else - { - Assert.Equal(testCase.ExpectedHttpRoute, metricHttpRoute); - } + Assert.Equal(expectedActivityDisplayName, activity.DisplayName); - // Only produce README files based on final semantic conventions - if (!useLegacyConventions) + var testResult = new RoutingTestResult { - var testResult = new RoutingTestResult - { - IdealHttpRoute = testCase.ExpectedHttpRoute, - ActivityDisplayName = activity.DisplayName, - ActivityHttpRoute = activityHttpRoute, - MetricHttpRoute = metricHttpRoute, - TestCase = testCase, - RouteInfo = RouteInfo.Current, - }; - - this.fixture.AddTestResult(testResult); - } + IdealHttpRoute = testCase.ExpectedHttpRoute, + ActivityDisplayName = activity.DisplayName, + ActivityHttpRoute = activityHttpRoute, + MetricHttpRoute = metricHttpRoute, + TestCase = testCase, + RouteInfo = RouteInfo.Current, + }; + + this.fixture.AddTestResult(testResult); } - private static void GetTagsFromActivity(bool useLegacyConventions, Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute) + private static void GetTagsFromActivity(Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute) { - var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode; - var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod; + var expectedStatusCodeKey = HttpStatusCode; + var expectedHttpMethodKey = HttpMethod; httpStatusCode = Convert.ToInt32(activity.GetTagItem(expectedStatusCodeKey)); httpMethod = (activity.GetTagItem(expectedHttpMethodKey) as string)!; httpRoute = activity.GetTagItem(HttpRoute) as string ?? string.Empty; @@ -165,8 +128,8 @@ private static void GetTagsFromActivity(bool useLegacyConventions, Activity acti private static void GetTagsFromMetricPoint(bool useLegacyConventions, MetricPoint metricPoint, out int httpStatusCode, out string httpMethod, out string? httpRoute) { - var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode; - var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod; + var expectedStatusCodeKey = HttpStatusCode; + var expectedHttpMethodKey = HttpMethod; httpStatusCode = 0; httpMethod = string.Empty; diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs index f2b34dee76d..66ad69a281c 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs @@ -64,11 +64,11 @@ public static async Task WriteResponseAsync(Stream ms, TResponse resp data = response.ToByteArray(); } - await ResponseUtils.WriteHeaderAsync(ms, data.Length, compress, CancellationToken.None).ConfigureAwait(false); + await ResponseUtils.WriteHeaderAsync(ms, data.Length, compress, CancellationToken.None); #if NET5_0_OR_GREATER - await ms.WriteAsync(data).ConfigureAwait(false); + await ms.WriteAsync(data); #else - await ms.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + await ms.WriteAsync(data, 0, data.Length); #endif } @@ -78,7 +78,7 @@ private static async Task CreateResponseContentCore(TR var ms = new MemoryStream(); foreach (var response in responses) { - await WriteResponseAsync(ms, response, compressionProvider).ConfigureAwait(false); + await WriteResponseAsync(ms, response, compressionProvider); } ms.Seek(0, SeekOrigin.Begin); diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs index e104f11acbc..e22fb6d7e17 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs @@ -37,8 +37,8 @@ public static TestHttpMessageHandler Create(Func tcs.TrySetCanceled()); - var result = await Task.WhenAny(sendAsync(request), tcs.Task).ConfigureAwait(false); - return await result.ConfigureAwait(false); + var result = await Task.WhenAny(sendAsync(request), tcs.Task); + return await result; }); } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs index 407b6d9ab39..a9a6ff901ed 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs @@ -55,7 +55,7 @@ public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool sho using var httpClient = ClientTestHelpers.CreateTestClient(async request => { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()); var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); response.TrailingHeaders().Add("grpc-message", "value"); return response; @@ -150,7 +150,7 @@ public void GrpcClientCallsAreCollectedSuccessfully_New(string baseAddress, bool using var httpClient = ClientTestHelpers.CreateTestClient(async request => { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()); var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); response.TrailingHeaders().Add("grpc-message", "value"); return response; @@ -246,7 +246,7 @@ public void GrpcClientCallsAreCollectedSuccessfully_Dupe(string baseAddress, boo using var httpClient = ClientTestHelpers.CreateTestClient(async request => { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()); var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); response.TrailingHeaders().Add("grpc-message", "value"); return response; diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs index c8011b4bb84..da734da25f9 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs @@ -51,77 +51,6 @@ public GrpcTests() [InlineData(true)] [InlineData(false)] public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcAspNetCoreSupport) - { - var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); - - if (enableGrpcAspNetCoreSupport.HasValue) - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(options => - { - options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; - }); - } - else - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(); - } - - using var tracerProvider = tracerProviderBuilder - .AddInMemoryExporter(exportedItems) - .Build(); - - var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; - var uri = new Uri($"http://localhost:{this.server.Port}"); - - using var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var returnMsg = client.SayHello(new HelloRequest()).Message; - Assert.False(string.IsNullOrEmpty(returnMsg)); - - WaitForExporterToReceiveItems(exportedItems, 1); - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - Assert.Equal(ActivityKind.Server, activity.Kind); - - if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value) - { - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp), clientLoopbackAddresses); - Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } - else - { - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - } - - Assert.Equal(Status.Unset, activity.GetStatus()); - - // The following are http.* attributes that are also included on the span for the gRPC invocation. - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeNetHostPort)); - Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); - Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); - Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); - } - - // Tests for v1.21.0 Semantic Conventions for database client calls. - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md - // This test emits the new attributes. - // This test method can replace the other (old) test method when this library is GA. - [Theory] - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_New(bool? enableGrpcAspNetCoreSupport) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) @@ -190,112 +119,6 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_New(bool? enableG Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string); } - // Tests for v1.21.0 Semantic Conventions for database client calls. - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md - // This test emits both the new and older attributes. - // This test method can be deleted when this library is GA. - [Theory] - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_Dupe(bool? enableGrpcAspNetCoreSupport) - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http/dup" }) - .Build(); - - var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)); - - if (enableGrpcAspNetCoreSupport.HasValue) - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(options => - { - options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; - }); - } - else - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(); - } - - using var tracerProvider = tracerProviderBuilder - .AddInMemoryExporter(exportedItems) - .Build(); - - var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; - var uri = new Uri($"http://localhost:{this.server.Port}"); - - using var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var returnMsg = client.SayHello(new HelloRequest()).Message; - Assert.False(string.IsNullOrEmpty(returnMsg)); - - WaitForExporterToReceiveItems(exportedItems, 1); - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - Assert.Equal(ActivityKind.Server, activity.Kind); - - // OLD - if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value) - { - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp), clientLoopbackAddresses); - Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } - else - { - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - } - - Assert.Equal(Status.Unset, activity.GetStatus()); - - // The following are http.* attributes that are also included on the span for the gRPC invocation. - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeNetHostPort)); - Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); - Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); - Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); - - // NEW - if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value) - { - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeClientAddress), clientLoopbackAddresses); - Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeClientPort)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } - else - { - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - } - - Assert.Equal(Status.Unset, activity.GetStatus()); - - // The following are http.* attributes that are also included on the span for the gRPC invocation. - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); - Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); - Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); - Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeUrlPath)); - Assert.Equal("2", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); - Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string); - } - #if NET6_0_OR_GREATER [Theory(Skip = "Skipping for .NET 6 and higher due to bug #3023")] #endif diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs index cf444b2a2c5..70c6ff5636e 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs @@ -42,10 +42,10 @@ public override async Task SayHellos(HelloRequest request, IServerStreamWriter using System.Diagnostics; -using Microsoft.Extensions.Configuration; #if NETFRAMEWORK using System.Net; using System.Net.Http; @@ -30,8 +29,6 @@ using Xunit; using Xunit.Abstractions; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; - namespace OpenTelemetry.Instrumentation.Http.Tests; public partial class HttpClientTests : IDisposable @@ -174,7 +171,7 @@ public async Task InjectsHeadersAsync(bool shouldEnrich) .Build()) { using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); } Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. @@ -248,7 +245,7 @@ public async Task InjectsHeadersAsync_CustomFormat() .Build()) { using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); } Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. @@ -318,7 +315,7 @@ public async Task RespectsSuppress() using var c = new HttpClient(); using (SuppressInstrumentationScope.Begin()) { - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); } } @@ -357,7 +354,7 @@ public async Task ExportsSpansCreatedForRetries() using var clientHandler = new HttpClientHandler(); using var retryHandler = new RetryHandler(clientHandler, maxRetries); using var httpClient = new HttpClient(retryHandler); - await httpClient.SendAsync(request).ConfigureAwait(false); + await httpClient.SendAsync(request); // number of exported spans should be 3(maxRetries) Assert.Equal(maxRetries, exportedItems.Count()); @@ -393,12 +390,7 @@ public async Task HttpRequestMethodIsSetOnActivityAsPerSpec(string originalMetho Method = new HttpMethod(originalMethod), }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) - .Build(); - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddHttpClientInstrumentation() .AddInMemoryExporter(exportedItems) .Build(); @@ -407,7 +399,7 @@ public async Task HttpRequestMethodIsSetOnActivityAsPerSpec(string originalMetho try { - await httpClient.SendAsync(request).ConfigureAwait(false); + await httpClient.SendAsync(request); } catch { @@ -453,12 +445,7 @@ public async Task HttpRequestMethodIsSetonRequestDurationMetricAsPerSpec(string Method = new HttpMethod(originalMethod), }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) - .Build(); - using var meterprovider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) .AddHttpClientInstrumentation() .AddInMemoryExporter(metricItems) .Build(); @@ -467,7 +454,7 @@ public async Task HttpRequestMethodIsSetonRequestDurationMetricAsPerSpec(string try { - await httpClient.SendAsync(request).ConfigureAwait(false); + await httpClient.SendAsync(request); } catch { @@ -511,7 +498,7 @@ public async Task RedirectTest() .Build()) { using var c = new HttpClient(); - await c.GetAsync($"{this.url}redirect").ConfigureAwait(false); + await c.GetAsync($"{this.url}redirect"); } #if NETFRAMEWORK @@ -523,15 +510,15 @@ public async Task RedirectTest() Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. var firstActivity = (Activity)processor.Invocations[2].Arguments[0]; // First OnEnd - Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); + Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200); #else Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnStart/OnEnd/OnShutdown/Dispose called. var firstActivity = (Activity)processor.Invocations[2].Arguments[0]; // First OnEnd - Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 302); + Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 302); var secondActivity = (Activity)processor.Invocations[4].Arguments[0]; // Second OnEnd - Assert.Contains(secondActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); + Assert.Contains(secondActivity.TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200); #endif } @@ -562,7 +549,7 @@ public async void RequestNotCollectedWhenInstrumentationFilterApplied() .Build()) { using var c = new HttpClient(); - await c.GetAsync(this.url).ConfigureAwait(false); + await c.GetAsync(this.url); } #if NETFRAMEWORK @@ -593,7 +580,7 @@ public async void RequestNotCollectedWhenInstrumentationFilterThrowsException() { using var c = new HttpClient(); using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log); - await c.GetAsync(this.url).ConfigureAwait(false); + await c.GetAsync(this.url); Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } @@ -614,7 +601,7 @@ public async Task ReportsExceptionEventForNetworkFailuresWithGetAsync() using var c = new HttpClient(); try { - await c.GetAsync("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/").ConfigureAwait(false); + await c.GetAsync("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); } catch { @@ -640,7 +627,7 @@ public async Task DoesNotReportExceptionEventOnErrorResponseWithGetAsync() using var c = new HttpClient(); try { - await c.GetAsync($"{this.url}500").ConfigureAwait(false); + await c.GetAsync($"{this.url}500"); } catch { @@ -671,7 +658,7 @@ public async Task DoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() using var c = new HttpClient(); try { - await c.GetStringAsync($"{this.url}500").ConfigureAwait(false); + await c.GetStringAsync($"{this.url}500"); } catch { @@ -746,7 +733,7 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) }; using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); parent?.Stop(); diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs index c957c14840d..b7d381c56ef 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs @@ -22,12 +22,9 @@ using System.Reflection; using System.Text.Json; #endif -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Xunit; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.Http.Tests; @@ -35,45 +32,16 @@ public partial class HttpClientTests { public static readonly IEnumerable TestData = HttpTestData.ReadTestCases(); -#if !NET8_0_OR_GREATER - [Theory] - [MemberData(nameof(TestData))] - public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsOldSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc) - { - await HttpOutCallsAreCollectedSuccessfullyBodyAsync( - this.host, - this.port, - tc, - enableTracing: true, - enableMetrics: true, - semanticConvention: HttpSemanticConvention.Old).ConfigureAwait(false); - } - [Theory] [MemberData(nameof(TestData))] - public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsDuplicateSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc) + public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc) { await HttpOutCallsAreCollectedSuccessfullyBodyAsync( this.host, this.port, tc, enableTracing: true, - enableMetrics: true, - semanticConvention: HttpSemanticConvention.Dupe).ConfigureAwait(false); - } -#endif - - [Theory] - [MemberData(nameof(TestData))] - public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsNewSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc) - { - await HttpOutCallsAreCollectedSuccessfullyBodyAsync( - this.host, - this.port, - tc, - enableTracing: true, - enableMetrics: true, - semanticConvention: HttpSemanticConvention.New).ConfigureAwait(false); + enableMetrics: true); } [Theory] @@ -85,7 +53,7 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync( this.port, tc, enableTracing: false, - enableMetrics: true).ConfigureAwait(false); + enableMetrics: true); } [Theory] @@ -97,7 +65,7 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync( this.port, tc, enableTracing: true, - enableMetrics: false).ConfigureAwait(false); + enableMetrics: false); } [Theory] @@ -109,7 +77,7 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync( this.port, tc, enableTracing: false, - enableMetrics: false).ConfigureAwait(false); + enableMetrics: false); } #if !NET8_0_OR_GREATER @@ -129,29 +97,29 @@ public async Task DebugIndividualTestAsync() ""spanStatus"": ""Unset"", ""spanKind"": ""Client"", ""spanAttributes"": { - ""http.scheme"": ""http"", - ""http.method"": ""GET"", - ""net.peer.name"": ""{host}"", - ""net.peer.port"": ""{port}"", - ""http.status_code"": ""399"", - ""http.flavor"": ""{flavor}"", - ""http.url"": ""http://{host}:{port}/"" + ""url.scheme"": ""http"", + ""http.request.method"": ""GET"", + ""server.address"": ""{host}"", + ""server.port"": ""{port}"", + ""http.response.status_code"": ""399"", + ""network.protocol.version"": ""{flavor}"", + ""url.full"": ""http://{host}:{port}/"" } } ] ", new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var t = (Task)this.GetType().InvokeMember(nameof(this.HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsOldSemanticConventionsAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First()); - await t.ConfigureAwait(false); + var t = (Task)this.GetType().InvokeMember(nameof(this.HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First()); + await t; } #endif [Fact] public async Task CheckEnrichmentWhenSampling() { - await CheckEnrichment(new AlwaysOffSampler(), false, this.url).ConfigureAwait(false); - await CheckEnrichment(new AlwaysOnSampler(), true, this.url).ConfigureAwait(false); + await CheckEnrichment(new AlwaysOffSampler(), false, this.url); + await CheckEnrichment(new AlwaysOnSampler(), true, this.url); } #if NET8_0_OR_GREATER @@ -178,7 +146,7 @@ public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc) request.Headers.Add("contextRequired", "false"); request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString()); - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); } catch (Exception) { @@ -189,8 +157,6 @@ public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc) meterProvider.Dispose(); } - // dns.lookups.duration is a typo - // https://github.com/dotnet/runtime/issues/92917 var requestMetrics = metrics .Where(metric => metric.Name == "http.client.request.duration" || @@ -198,7 +164,7 @@ public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc) metric.Name == "http.client.request.time_in_queue" || metric.Name == "http.client.connection.duration" || metric.Name == "http.client.open_connections" || - metric.Name == "dns.lookups.duration") + metric.Name == "dns.lookup.duration") .ToArray(); if (tc.ResponseExpected) @@ -218,8 +184,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( int port, HttpTestData.HttpOutTestCase tc, bool enableTracing, - bool enableMetrics, - HttpSemanticConvention? semanticConvention = null) + bool enableMetrics) { bool enrichWithHttpWebRequestCalled = false; bool enrichWithHttpWebResponseCalled = false; @@ -234,9 +199,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( if (enableMetrics) { meterProviderBuilder - .AddHttpClientInstrumentation() - .ConfigureServices( - s => s.AddSingleton(BuildConfigurationWithSemanticConventionOptIn(semanticConvention))); + .AddHttpClientInstrumentation(); } var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); @@ -252,9 +215,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( opt.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; opt.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; opt.RecordException = tc.RecordException ?? false; - }) - .ConfigureServices( - s => s.AddSingleton(BuildConfigurationWithSemanticConventionOptIn(semanticConvention))); + }); } var metrics = new List(); @@ -288,7 +249,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( request.Headers.Add("contextRequired", "false"); request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString()); - await c.SendAsync(request).ConfigureAwait(false); + await c.SendAsync(request); } catch (Exception) { @@ -301,7 +262,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( } var requestMetrics = metrics - .Where(metric => metric.Name == "http.client.duration" || metric.Name == "http.client.request.duration") + .Where(metric => metric.Name == "http.client.request.duration") .ToArray(); var normalizedAttributesTestCase = tc.SpanAttributes.ToDictionary(x => x.Key, x => HttpTestData.NormalizeValues(x.Value, host, port)); @@ -341,65 +302,40 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString()); - int numberOfNewTags = activity.Status == ActivityStatusCode.Error ? 6 : 5; - int numberOfDupeTags = activity.Status == ActivityStatusCode.Error ? 12 : 11; + int numberOfTags = activity.Status == ActivityStatusCode.Error ? 5 : 4; - var expectedAttributeCount = semanticConvention == HttpSemanticConvention.Dupe - ? numberOfDupeTags + (tc.ResponseExpected ? 2 : 0) - : semanticConvention == HttpSemanticConvention.New - ? numberOfNewTags + (tc.ResponseExpected ? 1 : 0) - : 6 + (tc.ResponseExpected ? 1 : 0); + var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0); Assert.Equal(expectedAttributeCount, normalizedAttributes.Count); - if (semanticConvention == null || semanticConvention.Value.HasFlag(HttpSemanticConvention.Old)) + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpRequestMethod]); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerAddress]); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerPort]); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeUrlFull && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeUrlFull]); + if (tc.ResponseExpected) { - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetPeerName && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetPeerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpScheme]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpUrl && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpUrl]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpFlavor && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]); - if (tc.ResponseExpected) - { - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - } - else + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetworkProtocolVersion]); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); + + if (tc.ResponseCode >= 400) { - Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpStatusCode); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); } } - - if (semanticConvention != null && semanticConvention.Value.HasFlag(HttpSemanticConvention.New)) + else { - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeUrlFull && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpUrl]); - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]); - if (tc.ResponseExpected) - { - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - - if (tc.ResponseCode >= 400) - { - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - } - } - else - { - Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode); + Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode); + Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion); #if NET8_0_OR_GREATER - // we are using fake address so it will be "name_resolution_error" - // TODO: test other error types. - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error"); + // we are using fake address so it will be "name_resolution_error" + // TODO: test other error types. + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error"); #elif NETFRAMEWORK - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure"); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure"); #else - Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException"); + Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException"); #endif - } } if (tc.RecordException.HasValue && tc.RecordException.Value) @@ -415,215 +351,113 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( } else { - if (semanticConvention == HttpSemanticConvention.Dupe) - { - Assert.Equal(2, requestMetrics.Length); - } - else - { - Assert.Single(requestMetrics); - } - -#if !NET8_0_OR_GREATER - if (semanticConvention == null || semanticConvention.Value.HasFlag(HttpSemanticConvention.Old)) - { - var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.duration"); - Assert.NotNull(metric); - Assert.Equal("ms", metric.Unit); - Assert.True(metric.MetricType == MetricType.Histogram); - - var metricPoints = new List(); - foreach (var p in metric.GetMetricPoints()) - { - metricPoints.Add(p); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; + Assert.Single(requestMetrics); - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); + var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.request.duration"); + Assert.NotNull(metric); + Assert.Equal("s", metric.Unit); + Assert.True(metric.MetricType == MetricType.Histogram); - Assert.Equal(1L, count); - - if (enableTracing) - { - var activity = Assert.Single(activities); - Assert.Equal(activity.Duration.TotalMilliseconds, sum); - } - else - { - Assert.True(sum > 0); - } - - // Inspect Metric Attributes - var attributes = new Dictionary(); - foreach (var tag in metricPoint.Tags) - { - attributes[tag.Key] = tag.Value; - } - - var expectedAttributeCount = 5 + (tc.ResponseExpected ? 1 : 0); - - Assert.Equal(expectedAttributeCount, attributes.Count); - - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetPeerName && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetPeerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpScheme]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpFlavor && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]); - if (tc.ResponseExpected) - { - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - } - else - { - Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpStatusCode); - } - - // Inspect Histogram Bounds - var histogramBuckets = metricPoint.GetHistogramBuckets(); - var histogramBounds = new List(); - foreach (var t in histogramBuckets) - { - histogramBounds.Add(t.ExplicitBound); - } - - Assert.Equal( - expected: new List { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000, double.PositiveInfinity }, - actual: histogramBounds); - } -#endif - if (semanticConvention != null && semanticConvention.Value.HasFlag(HttpSemanticConvention.New)) + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) { - var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.request.duration"); - Assert.NotNull(metric); - Assert.Equal("s", metric.Unit); - Assert.True(metric.MetricType == MetricType.Histogram); - - var metricPoints = new List(); - foreach (var p in metric.GetMetricPoints()) - { - metricPoints.Add(p); - } + metricPoints.Add(p); + } - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); - Assert.Equal(1L, count); + Assert.Equal(1L, count); - if (enableTracing) - { - var activity = Assert.Single(activities); + if (enableTracing) + { + var activity = Assert.Single(activities); #if !NET8_0_OR_GREATER - Assert.Equal(activity.Duration.TotalSeconds, sum); + Assert.Equal(activity.Duration.TotalSeconds, sum); #endif - } - else - { - Assert.True(sum > 0); - } + } + else + { + Assert.True(sum > 0); + } - // Inspect Metric Attributes - var attributes = new Dictionary(); - foreach (var tag in metricPoint.Tags) - { - attributes[tag.Key] = tag.Value; - } + // Inspect Metric Attributes + var attributes = new Dictionary(); + foreach (var tag in metricPoint.Tags) + { + attributes[tag.Key] = tag.Value; + } -#if !NET8_0_OR_GREATER - var numberOfTags = 6; -#else - // network.protocol.version is not emitted when response if not received. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4928 - var numberOfTags = 5; -#endif - if (tc.ResponseExpected) - { - var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - numberOfTags = (expectedStatusCode >= 400) ? 6 : 5; - } + var numberOfTags = 4; + if (tc.ResponseExpected) + { + var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); + numberOfTags = (expectedStatusCode >= 400) ? 5 : 4; // error.type extra tag + } + else + { + numberOfTags = 5; // error.type would be extra + } - var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 1 : 0); + var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0); // responsecode + protocolversion - Assert.Equal(expectedAttributeCount, attributes.Count); + Assert.Equal(expectedAttributeCount, attributes.Count); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]); - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeUrlScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpScheme]); -#if !NET8_0_OR_GREATER - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]); -#endif + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpRequestMethod]); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerAddress]); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerPort]); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeUrlScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeUrlScheme]); - if (tc.ResponseExpected) - { - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); + if (tc.ResponseExpected) + { + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetworkProtocolVersion]); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); - if (tc.ResponseCode >= 400) - { - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]); - } - } - else + if (tc.ResponseCode >= 400) { - Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); + } + } + else + { + Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion); + Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode); #if NET8_0_OR_GREATER - // we are using fake address so it will be "name_resolution_error" - // TODO: test other error types. - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error"); + // we are using fake address so it will be "name_resolution_error" + // TODO: test other error types. + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error"); #elif NETFRAMEWORK - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure"); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure"); #else - Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException"); + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException"); #endif - } + } - // Inspect Histogram Bounds - var histogramBuckets = metricPoint.GetHistogramBuckets(); - var histogramBounds = new List(); - foreach (var t in histogramBuckets) - { - histogramBounds.Add(t.ExplicitBound); - } + // Inspect Histogram Bounds + var histogramBuckets = metricPoint.GetHistogramBuckets(); + var histogramBounds = new List(); + foreach (var t in histogramBuckets) + { + histogramBounds.Add(t.ExplicitBound); + } - // TODO: Remove the check for the older bounds once 1.7.0 is released. This is a temporary fix for instrumentation libraries CI workflow. + // TODO: Remove the check for the older bounds once 1.7.0 is released. This is a temporary fix for instrumentation libraries CI workflow. - var expectedHistogramBoundsOld = new List { 0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; - var expectedHistogramBoundsNew = new List { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; + var expectedHistogramBoundsOld = new List { 0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; + var expectedHistogramBoundsNew = new List { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; - var histogramBoundsMatchCorrectly = Enumerable.SequenceEqual(expectedHistogramBoundsOld, histogramBounds) || - Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds); + var histogramBoundsMatchCorrectly = Enumerable.SequenceEqual(expectedHistogramBoundsOld, histogramBounds) || + Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds); - Assert.True(histogramBoundsMatchCorrectly); - } + Assert.True(histogramBoundsMatchCorrectly); } } - private static IConfiguration BuildConfigurationWithSemanticConventionOptIn( - HttpSemanticConvention? semanticConvention) - { - var builder = new ConfigurationBuilder(); - - if (semanticConvention != null && semanticConvention != HttpSemanticConvention.Old) - { - builder.AddInMemoryCollection( - new Dictionary - { - ["OTEL_SEMCONV_STABILITY_OPT_IN"] = semanticConvention == HttpSemanticConvention.Dupe - ? "http/dup" - : "http", - }); - } - - return builder.Build(); - } - private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, string url) { bool enrichWithHttpWebRequestCalled = false; @@ -645,7 +479,7 @@ private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, .Build()) { using var c = new HttpClient(); - using var r = await c.GetAsync(url).ConfigureAwait(false); + using var r = await c.GetAsync(url); } if (enrichExpected) diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs index 074ae7df55c..be4f6a2909e 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs @@ -155,7 +155,7 @@ public async Task TestReflectInitializationViaSubscription() // Send a random Http request to generate some events using (var client = new HttpClient()) { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); + (await client.GetAsync(this.BuildRequestUrl())).Dispose(); } // Just make sure some events are written, to confirm we successfully subscribed to it. @@ -180,8 +180,8 @@ public async Task TestBasicReceiveAndResponseEvents(string method, string queryS using (var client = new HttpClient()) { (method == "GET" - ? await client.GetAsync(url).ConfigureAwait(false) - : await client.PostAsync(url, new StringContent("hello world")).ConfigureAwait(false)).Dispose(); + ? await client.GetAsync(url) + : await client.PostAsync(url, new StringContent("hello world"))).Dispose(); } // We should have exactly one Start and one Stop event @@ -211,8 +211,8 @@ public async Task TestBasicReceiveAndResponseEventsWithoutSampling(string method using (var client = new HttpClient()) { (method == "GET" - ? await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false) - : await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world")).ConfigureAwait(false)).Dispose(); + ? await client.GetAsync(this.BuildRequestUrl()) + : await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world"))).Dispose(); } // There should be no events because we turned off sampling. @@ -248,7 +248,7 @@ public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int stream = webRequest.GetRequestStream(); break; case 1: - stream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false); + stream = await webRequest.GetRequestStreamAsync(); break; case 2: { @@ -310,7 +310,7 @@ public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int webResponse = webRequest.GetResponse(); break; case 1: - webResponse = await webRequest.GetResponseAsync().ConfigureAwait(false); + webResponse = await webRequest.GetResponseAsync(); break; case 2: { @@ -397,7 +397,7 @@ public async Task TestTraceStateAndBaggage() // Send a random Http request to generate some events using (var client = new HttpClient()) { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); + (await client.GetAsync(this.BuildRequestUrl())).Dispose(); } parent.Stop(); @@ -438,7 +438,7 @@ public async Task DoNotInjectTraceParentWhenPresent(string method) request.Content = new StringContent("hello world"); } - (await client.SendAsync(request).ConfigureAwait(false)).Dispose(); + (await client.SendAsync(request)).Dispose(); } // No events are sent. @@ -466,8 +466,8 @@ public async Task TestResponseWithoutContentEvents(string method) using (var client = new HttpClient()) { using HttpResponseMessage response = method == "GET" - ? await client.GetAsync(url).ConfigureAwait(false) - : await client.PostAsync(url, new StringContent("hello world")).ConfigureAwait(false); + ? await client.GetAsync(url) + : await client.PostAsync(url, new StringContent("hello world")); } // We should have exactly one Start and one Stop event @@ -499,8 +499,8 @@ public async Task TestRedirectedRequest(string method) using (var client = new HttpClient()) { using HttpResponseMessage response = method == "GET" - ? await client.GetAsync(this.BuildRequestUrl(queryString: "redirects=10")).ConfigureAwait(false) - : await client.PostAsync(this.BuildRequestUrl(queryString: "redirects=10"), new StringContent("hello world")).ConfigureAwait(false); + ? await client.GetAsync(this.BuildRequestUrl(queryString: "redirects=10")) + : await client.PostAsync(this.BuildRequestUrl(queryString: "redirects=10"), new StringContent("hello world")); } // We should have exactly one Start and one Stop event @@ -529,7 +529,7 @@ public async Task TestRequestWithException(string method) return method == "GET" ? new HttpClient().GetAsync(url) : new HttpClient().PostAsync(url, new StringContent("hello world")); - }).ConfigureAwait(false); + }); // check that request failed because of the wrong domain name and not because of reflection var webException = (WebException)ex.InnerException; @@ -572,7 +572,7 @@ public async Task TestCanceledRequest(string method) return method == "GET" ? client.GetAsync(url, cts.Token) : client.PostAsync(url, new StringContent("hello world"), cts.Token); - }).ConfigureAwait(false); + }); Assert.True(ex is TaskCanceledException || ex is WebException); } @@ -611,7 +611,7 @@ public async Task TestSecureTransportFailureRequest(string method) return method == "GET" ? client.GetAsync(url) : client.PostAsync(url, new StringContent("hello world")); - }).ConfigureAwait(false); + }); Assert.True(ex is HttpRequestException); } @@ -653,7 +653,7 @@ public async Task TestSecureTransportRetryFailureRequest(string method) return method == "GET" ? client.GetAsync(url) : client.PostAsync(url, new StringContent("hello world")); - }).ConfigureAwait(false); + }); Assert.True(ex is HttpRequestException); } @@ -685,7 +685,7 @@ public async Task TestInvalidBaggage() using (var client = new HttpClient()) { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); + (await client.GetAsync(this.BuildRequestUrl())).Dispose(); } Assert.Equal(2, eventRecords.Records.Count()); @@ -699,7 +699,7 @@ public async Task TestInvalidBaggage() /// Test to make sure every event record has the right dynamic properties. /// [Fact] - public void TestMultipleConcurrentRequests() + public async Task TestMultipleConcurrentRequests() { ServicePointManager.DefaultConnectionLimit = int.MaxValue; using var parentActivity = new Activity("parent").Start(); @@ -724,13 +724,13 @@ public void TestMultipleConcurrentRequests() } // wait up to 10 sec for all requests and suppress exceptions - Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(tt => + await Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(async tt => { foreach (var task in tasks) { - task.Value.Result?.Dispose(); + (await task.Value)?.Dispose(); } - }).Wait(); + }); // Examine the result. Make sure we got all successful requests. @@ -776,20 +776,20 @@ private static void VerifyHeaders(HttpWebRequest startRequest) private static void VerifyActivityStartTags(string netPeerName, int? netPeerPort, string method, string url, Activity activity) { Assert.NotNull(activity.TagObjects); - Assert.Equal(method, activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); + Assert.Equal(method, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); if (netPeerPort != null) { - Assert.Equal(netPeerPort, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + Assert.Equal(netPeerPort, activity.GetTagValue(SemanticConventions.AttributeServerPort)); } - Assert.Equal(netPeerName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + Assert.Equal(netPeerName, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - Assert.Equal(url, activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); + Assert.Equal(url, activity.GetTagValue(SemanticConventions.AttributeUrlFull)); } private static void VerifyActivityStopTags(int statusCode, Activity activity) { - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); } private static void ActivityEnrichment(Activity activity, string method, object obj) diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs index 85ab9988335..40f0cf9a873 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs @@ -87,7 +87,7 @@ public async Task BacksOffIfAlreadyInstrumented() request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"); - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); #if NETFRAMEWORK // Note: Back-off is part of the .NET Framework reflection only and @@ -129,7 +129,7 @@ public async Task RequestNotCollectedWhenInstrumentationFilterApplied() request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); #if NETFRAMEWORK Assert.True(httpWebRequestFilterApplied); @@ -163,7 +163,7 @@ public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } @@ -190,7 +190,7 @@ public async Task InjectsHeadersAsync() parent.TraceStateString = "k1=v1,k2=v2"; parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; @@ -272,7 +272,7 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); parent?.Stop(); @@ -345,7 +345,7 @@ public async Task ReportsExceptionEventForNetworkFailures() request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); } catch { @@ -374,7 +374,7 @@ public async Task ReportsExceptionEventOnErrorResponse() request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); } catch { diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs index 44d79860889..64166560e0a 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs @@ -101,7 +101,7 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc x => x.Key, x => { - if (x.Key == "http.flavor") + if (x.Key == "network.protocol.version") { return "1.1"; } @@ -127,6 +127,12 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc continue; } + if (tag.Key == SemanticConventions.AttributeErrorType) + { + // TODO: Add validation for error.type in test cases. + continue; + } + Assert.Fail($"Tag {tag.Key} was not found in test data."); } @@ -173,13 +179,13 @@ public void DebugIndividualTest() ""spanKind"": ""Client"", ""setHttpFlavor"": true, ""spanAttributes"": { - ""http.scheme"": ""http"", - ""http.method"": ""GET"", - ""net.peer.name"": ""{host}"", - ""net.peer.port"": ""{port}"", - ""http.flavor"": ""1.1"", - ""http.status_code"": ""200"", - ""http.url"": ""http://{host}:{port}/"" + ""url.scheme"": ""http"", + ""http.request.method"": ""GET"", + ""server.address"": ""{host}"", + ""server.port"": ""{port}"", + ""network.protocol.version"": ""1.1"", + ""http.response.status_code"": ""200"", + ""url.full"": ""http://{host}:{port}/"" } } ", diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs index 1ba12c16768..7d3b28b3b75 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs @@ -41,7 +41,7 @@ protected override async Task SendAsync( try { - response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + response = await base.SendAsync(request, cancellationToken); } catch { diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json index d808d5c00f2..056a4d56098 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json @@ -7,13 +7,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/" } }, { @@ -24,13 +24,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "POST", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "POST", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/" } }, { @@ -42,13 +42,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/path/to/resource/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/path/to/resource/" } }, { @@ -60,17 +60,17 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/path/to/resource#fragment" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/path/to/resource#fragment" } }, { - "name": "http.url must not contain username nor password", + "name": "url.full must not contain username nor password", "method": "GET", "url": "http://username:password@{host}:{port}/path/to/resource#fragment", "responseCode": 200, @@ -78,13 +78,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/path/to/resource#fragment" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/path/to/resource#fragment" } }, { @@ -96,12 +96,12 @@ "responseExpected": false, "recordException": false, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "sdlfaldfjalkdfjlkajdflkajlsdjf", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "url.full": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/" } }, { @@ -113,12 +113,12 @@ "responseExpected": false, "recordException": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "sdlfaldfjalkdfjlkajdflkajlsdjf", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "url.full": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/" } }, { @@ -130,13 +130,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/" } }, { @@ -148,13 +148,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/" } }, { @@ -166,13 +166,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "399", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "399", + "url.full": "http://{host}:{port}/" } }, { @@ -184,13 +184,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "400", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "400", + "url.full": "http://{host}:{port}/" } }, { @@ -202,13 +202,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "401", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "401", + "url.full": "http://{host}:{port}/" } }, { @@ -220,13 +220,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "403", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "403", + "url.full": "http://{host}:{port}/" } }, { @@ -238,13 +238,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "404", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "404", + "url.full": "http://{host}:{port}/" } }, { @@ -256,13 +256,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "429", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "429", + "url.full": "http://{host}:{port}/" } }, { @@ -274,13 +274,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "501", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "501", + "url.full": "http://{host}:{port}/" } }, { @@ -292,13 +292,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "503", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "503", + "url.full": "http://{host}:{port}/" } }, { @@ -310,13 +310,13 @@ "spanStatus": "Error", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "504", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "504", + "url.full": "http://{host}:{port}/" } }, { @@ -328,13 +328,13 @@ "spanStatus": "Unset", "responseExpected": true, "spanAttributes": { - "http.scheme": "http", - "http.method": "GET", - "net.peer.name": "{host}", - "net.peer.port": "{port}", - "http.flavor": "{flavor}", - "http.status_code": "200", - "http.url": "http://{host}:{port}/" + "url.scheme": "http", + "http.request.method": "GET", + "server.address": "{host}", + "server.port": "{port}", + "network.protocol.version": "{flavor}", + "http.response.status_code": "200", + "url.full": "http://{host}:{port}/" } } ] diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs index 131f5622b02..2df68ac8d31 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs @@ -59,7 +59,7 @@ public async Task SuccessfulCommandTest(CommandType commandType, string commandT using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); - await sqlConnection.OpenAsync().ConfigureAwait(false); + await sqlConnection.OpenAsync(); string dataSource = sqlConnection.DataSource; @@ -72,7 +72,7 @@ public async Task SuccessfulCommandTest(CommandType commandType, string commandT try { - await sqlCommand.ExecuteNonQueryAsync().ConfigureAwait(false); + await sqlCommand.ExecuteNonQueryAsync(); } catch { diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile index 0508036d3e8..9723867f4f3 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile @@ -2,8 +2,8 @@ # This should be run from the root of the repo: # docker build --file test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile . -ARG BUILD_SDK_VERSION=7.0 -ARG TEST_SDK_VERSION=7.0 +ARG BUILD_SDK_VERSION=8.0 +ARG TEST_SDK_VERSION=8.0 FROM ubuntu AS w3c #Install git @@ -13,7 +13,7 @@ RUN git clone --branch level-1 https://github.com/w3c/trace-context.git FROM mcr.microsoft.com/dotnet/sdk:${BUILD_SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net7.0 +ARG PUBLISH_FRAMEWORK=net8.0 WORKDIR /repo COPY . ./ WORKDIR "/repo/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests" diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs index e7ae8a783d3..05d8bd919d5 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs @@ -71,7 +71,7 @@ public void W3CTraceContextTestSuiteAsync(string value) Encoding.UTF8, "application/json"), }; - await this.httpClient.SendAsync(request).ConfigureAwait(false); + await this.httpClient.SendAsync(request); } } else diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/README.md b/test/OpenTelemetry.Tests.Stress.Metrics/README.md index c05f1c6758e..13ce3faf073 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/README.md +++ b/test/OpenTelemetry.Tests.Stress.Metrics/README.md @@ -15,5 +15,5 @@ for `Counter` and uncomment everything related to `Histogram` in the Open a console, run the following command from the current folder: ```sh -dotnet run --framework net7.0 --configuration Release +dotnet run --framework net8.0 --configuration Release ``` diff --git a/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs new file mode 100644 index 00000000000..65eb44c8145 --- /dev/null +++ b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs @@ -0,0 +1,70 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.Coyote; +using Microsoft.Coyote.SystematicTesting; +using OpenTelemetry.Metrics.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Tests.Concurrency; + +public class MetricsConcurrencyTests +{ + private readonly ITestOutputHelper output; + private readonly AggregatorTests aggregatorTests; + + public MetricsConcurrencyTests(ITestOutputHelper output) + { + this.output = output; + this.aggregatorTests = new AggregatorTests(); + } + + [SkipUnlessEnvVarFoundFact("OTEL_RUN_COYOTE_TESTS")] + [Trait("CategoryName", "CoyoteConcurrencyTests")] + public void MultithreadedLongHistogramTestConcurrencyTest() + { + var config = Configuration.Create() + .WithTestingIterations(100) + .WithMemoryAccessRaceCheckingEnabled(true); + + var test = TestingEngine.Create(config, this.aggregatorTests.MultiThreadedHistogramUpdateAndSnapShotTest); + + test.Run(); + + this.output.WriteLine(test.GetReport()); + this.output.WriteLine($"Bugs, if any: {string.Join("\n", test.TestReport.BugReports)}"); + + var dir = Directory.GetCurrentDirectory(); + if (test.TryEmitReports(dir, $"{nameof(this.MultithreadedLongHistogramTestConcurrencyTest)}_CoyoteOutput", out var reportPaths)) + { + foreach (var reportPath in reportPaths) + { + this.output.WriteLine($"Execution Report: {reportPath}"); + } + } + + if (test.TryEmitCoverageReports(dir, $"{nameof(this.MultithreadedLongHistogramTestConcurrencyTest)}_CoyoteOutput", out reportPaths)) + { + foreach (var reportPath in reportPaths) + { + this.output.WriteLine($"Coverage report: {reportPath}"); + } + } + + Assert.Equal(0, test.TestReport.NumOfFoundBugs); + } +} diff --git a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs b/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs index 2fc427e3ff0..d8307c1dab9 100644 --- a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs +++ b/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs @@ -129,7 +129,7 @@ public async Task CpuPressureTest() tasks.Add(Task.Run(async () => { - await Task.Delay(2000).ConfigureAwait(false); + await Task.Delay(2000); if (tid == 0) { @@ -167,6 +167,6 @@ public async Task CpuPressureTest() })); } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks); } } diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs index fd330dd0694..8d90505ffad 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs @@ -190,7 +190,7 @@ public async Task ExportTest(bool warmup) { Random random = new Random(); - await Task.Delay(random.Next(100, 150)).ConfigureAwait(false); + await Task.Delay(random.Next(100, 150)); for (int i = 0; i < 1000; i++) { @@ -201,12 +201,12 @@ public async Task ExportTest(bool warmup) // This should no-op mostly. pool.Return(logRecord); - await Task.Delay(random.Next(0, 20)).ConfigureAwait(false); + await Task.Delay(random.Next(0, 20)); } })); } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks); processor.ForceFlush(); @@ -248,7 +248,7 @@ public async Task DeadlockTest() { tasks.Add(Task.Run(async () => { - await Task.Delay(2000).ConfigureAwait(false); + await Task.Delay(2000); for (int i = 0; i < 100_000; i++) { @@ -259,7 +259,7 @@ public async Task DeadlockTest() })); } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks); Assert.True(pool.Count <= LogRecordSharedPool.DefaultMaxPoolSize); } diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs index a93d0156f0f..f3a171df6a9 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs @@ -29,16 +29,15 @@ public abstract class AggregatorTestsBase private static readonly MetricStreamIdentity MetricStreamIdentity = new(Instrument, HistogramConfiguration); private readonly bool emitOverflowAttribute; + private readonly bool shouldReclaimUnusedMetricPoints; private readonly AggregatorStore aggregatorStore; - protected AggregatorTestsBase(bool emitOverflowAttribute) + protected AggregatorTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { - if (emitOverflowAttribute) - { - this.emitOverflowAttribute = emitOverflowAttribute; - } + this.emitOverflowAttribute = emitOverflowAttribute; + this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; - this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute); + this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints); } [Fact] @@ -248,12 +247,14 @@ public void MultiThreadedHistogramUpdateAndSnapShotTest() [InlineData("Microsoft.AspNetCore.RateLimiting", "aspnetcore.rate_limiting.request.time_in_queue", "s", KnownHistogramBuckets.DefaultShortSeconds)] [InlineData("Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration", "s", KnownHistogramBuckets.DefaultLongSeconds)] [InlineData("Microsoft.AspNetCore.Server.Kestrel", "kestrel.tls_handshake.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] + [InlineData("OpenTelemetry.Instrumentation.AspNet", "http.server.duration", "ms", KnownHistogramBuckets.Default)] + [InlineData("OpenTelemetry.Instrumentation.AspNet", "http.server.request.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] [InlineData("OpenTelemetry.Instrumentation.AspNetCore", "http.server.duration", "ms", KnownHistogramBuckets.Default)] [InlineData("OpenTelemetry.Instrumentation.Http", "http.client.duration", "ms", KnownHistogramBuckets.Default)] [InlineData("System.Net.Http", "http.client.connection.duration", "s", KnownHistogramBuckets.DefaultLongSeconds)] [InlineData("System.Net.Http", "http.client.request.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] [InlineData("System.Net.Http", "http.client.request.time_in_queue", "s", KnownHistogramBuckets.DefaultShortSeconds)] - [InlineData("System.Net.NameResolution", "dns.lookups.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] + [InlineData("System.Net.NameResolution", "dns.lookup.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] [InlineData("General.App", "simple.alternative.counter", "s", KnownHistogramBuckets.Default)] public void HistogramBucketsDefaultUpdatesForSecondsTest(string meterName, string instrumentName, string unit, KnownHistogramBuckets expectedHistogramBuckets) { @@ -268,7 +269,8 @@ public void HistogramBucketsDefaultUpdatesForSecondsTest(string meterName, strin AggregationType.Histogram, AggregationTemporality.Cumulative, maxMetricPoints: 1024, - this.emitOverflowAttribute); + this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints); KnownHistogramBuckets actualHistogramBounds = KnownHistogramBuckets.Default; if (aggregatorStore.HistogramBounds == Metric.DefaultHistogramBoundsShortSeconds) @@ -345,6 +347,7 @@ internal void ExponentialHistogramTests(AggregationType aggregationType, Aggrega aggregationTemporality, maxMetricPoints: 1024, this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints, exemplarsEnabled ? new AlwaysOnExemplarFilter() : null); var expectedHistogram = new Base2ExponentialBucketHistogram(); @@ -453,7 +456,8 @@ internal void ExponentialMaxScaleConfigWorks(int? maxScale) AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, maxMetricPoints: 1024, - this.emitOverflowAttribute); + this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints); aggregatorStore.Update(10, Array.Empty>()); @@ -529,7 +533,7 @@ private class ThreadArguments public class AggregatorTests : AggregatorTestsBase { public AggregatorTests() - : base(false) + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -537,7 +541,23 @@ public AggregatorTests() public class AggregatorTestsWithOverflowAttribute : AggregatorTestsBase { public AggregatorTestsWithOverflowAttribute() - : base(true) + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class AggregatorTestsWithReclaimAttribute : AggregatorTestsBase +{ + public AggregatorTestsWithReclaimAttribute() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class AggregatorTestsWithBothReclaimAndOverflowAttributes : AggregatorTestsBase +{ + public AggregatorTestsWithBothReclaimAndOverflowAttributes() + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs index 9a980e12ea4..be2e564a3dd 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs @@ -14,6 +14,9 @@ // limitations under the License. // +using System.Diagnostics.Metrics; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Metrics.Tests; @@ -45,4 +48,77 @@ public void BuilderTypeDoesNotChangeTest() Assert.NotNull(provider); } + + [Theory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlushAfterEachTest) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + var meterName = Utils.GetCurrentMethodName(); + var exportedItems = new List(); + + var builder = Sdk.CreateMeterProviderBuilder() + .SetMaxMetricStreams(1) + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems); + + if (withView) + { + builder.AddView(i => null); + } + + using var meterProvider = builder + .Build() as MeterProviderSdk; + + Assert.NotNull(meterProvider); + + RunTest(); + + if (forceFlushAfterEachTest) + { + Assert.Single(exportedItems); + } + + RunTest(); + + if (forceFlushAfterEachTest) + { + Assert.Empty(exportedItems); + } + else + { + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + } + +#if DEBUG + // Note: This is inside a debug block because when running in CI the + // event source sees events from other tests running in parallel. + var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33); + + Assert.Single(metricInstrumentIgnoredEvents); +#endif + + void RunTest() + { + exportedItems.Clear(); + + var meter = new Meter(meterName); + + var counter = meter.CreateCounter("Counter"); + counter.Add(1); + + meter.Dispose(); + + if (forceFlushAfterEachTest) + { + meterProvider.ForceFlush(); + } + } + } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index bfe2229dcf5..8eb3a124dbf 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -16,6 +16,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; using OpenTelemetry.Tests; @@ -26,7 +27,7 @@ namespace OpenTelemetry.Metrics.Tests; #pragma warning disable SA1402 -public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable +public abstract class MetricApiTestsBase : MetricTestsBase { private const int MaxTimeToAllowForFlush = 10000; private static readonly int NumberOfThreads = Environment.ProcessorCount; @@ -35,14 +36,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable private static readonly int NumberOfMetricUpdateByEachThread = 100000; private readonly ITestOutputHelper output; - protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute) + protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + : base(BuildConfiguration(emitOverflowAttribute, shouldReclaimUnusedMetricPoints)) { this.output = output; - - if (emitOverflowAttribute) - { - Environment.SetEnvironmentVariable(EmitOverFlowAttributeConfigKey, "true"); - } } [Fact] @@ -50,10 +47,10 @@ public void MeasurementWithNullValuedTag() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("myCounter"); counter.Add(100, new KeyValuePair("tagWithNullValue", null)); @@ -83,10 +80,10 @@ public void ObserverCallbackTest() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter.CreateObservableGauge("myGauge", () => measurement); @@ -112,10 +109,10 @@ public void ObserverCallbackExceptionTest() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter.CreateObservableGauge("myGauge", () => measurement); @@ -146,11 +143,10 @@ public void MetricUnitIsExportedCorrectly(string unit) var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("name1", unit); counter.Add(10); @@ -169,11 +165,10 @@ public void MetricDescriptionIsExportedCorrectly(string description) var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("name1", null, description); counter.Add(10); @@ -189,11 +184,10 @@ public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -223,11 +217,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription1"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription2"); @@ -270,11 +263,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit1", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit2", "instrumentDescription"); @@ -317,11 +309,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -362,11 +353,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateHistogram("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -409,12 +399,11 @@ public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVers using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterLong = meter1.CreateCounter("name1"); @@ -442,20 +431,22 @@ public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddMeter(meter2.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = temporality; - }); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); - } + builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + }); - using var meterProvider = meterProviderBuilder.Build(); + if (hasView) + { + builder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); + } + }); // Expecting one metric stream. var counterLong = meter1.CreateCounter("name1"); @@ -473,6 +464,7 @@ public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe Assert.Equal(2, exportedItems.Count); } +#if !BUILDING_HOSTING_TESTS [Theory] [InlineData(true)] [InlineData(false)] @@ -486,18 +478,20 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) using var meter6 = new Meter("SomeCompany.SomeProduct.SomeComponent"); var exportedItems = new List(); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter("AbcCompany.XyzProduct.Component?") - .AddMeter("DefCompany.*.ComponentC") - .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. - .AddInMemoryExporter(exportedItems); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("myGauge1", "newName"); - } + builder + .AddMeter("AbcCompany.XyzProduct.Component?") + .AddMeter("DefCompany.*.ComponentC") + .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. + .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + if (hasView) + { + builder.AddView("myGauge1", "newName"); + } + }); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter1.CreateObservableGauge("myGauge1", () => measurement); @@ -525,6 +519,7 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) Assert.Equal("myGauge4", exportedItems[3].Name); Assert.Equal("myGauge5", exportedItems[4].Name); } +#endif [Theory] [InlineData(true)] @@ -535,15 +530,18 @@ public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}"); var exportedItems = new List(); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddInMemoryExporter(exportedItems); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("gauge1", "renamed"); - } + builder + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + builder.AddView("gauge1", "renamed"); + } + }); - using var meterProvider = meterProviderBuilder.Build(); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter1.CreateObservableGauge("myGauge1", () => measurement); @@ -564,13 +562,13 @@ public void CounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); counterLong.Add(10); counterLong.Add(10); @@ -666,13 +664,12 @@ public void ObservableCounterAggregationTest(bool exportDelta) }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); long sumReceived = GetLongSum(exportedItems); @@ -740,13 +737,12 @@ public void ObservableCounterWithTagsAggregationTest(bool exportDelta) }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Export 1 meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -837,14 +833,13 @@ public void ObservableCounterSpatialAggregationTest(bool exportDelta) }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; }) - .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty() }) - .Build(); + .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty() })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Single(exportedItems); @@ -878,13 +873,13 @@ public void UpDownCounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateUpDownCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); counterLong.Add(10); counterLong.Add(-5); @@ -960,13 +955,12 @@ public void ObservableUpDownCounterAggregationTest(bool exportDelta) }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); long sumReceived = GetLongSum(exportedItems); @@ -1024,13 +1018,12 @@ public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Export 1 meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -1094,13 +1087,13 @@ public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Emit the first metric with the sorted order of tag keys counterLong.Add(5, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); @@ -1185,13 +1178,13 @@ public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Emit the first metric with the unsorted order of tag keys counterLong.Add(5, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); @@ -1278,14 +1271,14 @@ public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2"); var counter1 = meter1.CreateCounter("counterFromMeter1"); var counter2 = meter2.CreateCounter("counterFromMeter2"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); + })); counter1.Add(10, new KeyValuePair("key", "value")); counter2.Add(10, new KeyValuePair("key", "value")); @@ -1346,13 +1339,13 @@ int MetricPointCount() using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); var counterLong = meter.CreateCounter("mycounterCapTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); + })); // Make one Add with no tags. // as currently we reserve 0th index @@ -1442,10 +1435,9 @@ public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter.CreateCounter(instrumentName); counterLong.Add(10); @@ -1464,10 +1456,9 @@ public void InstrumentWithValidNameIsExportedTest(string name) using var meter = new Meter("InstrumentValidNameIsExportedTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter.CreateCounter(name); counterLong.Add(10); @@ -1486,15 +1477,17 @@ public void SetupSdkProviderWithNoReader(bool hasViews) { // This test ensures that MeterProviderSdk can be set up without any reader using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name); - if (hasViews) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("counter", "renamedCounter"); - } + builder + .AddMeter(meter.Name); - using var meterProvider = meterProviderBuilder.Build(); + if (hasViews) + { + builder.AddView("counter", "renamedCounter"); + } + }); var counter = meter.CreateCounter("counter"); @@ -1506,10 +1499,10 @@ public void UnsupportedMetricInstrument() { using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { @@ -1518,16 +1511,39 @@ public void UnsupportedMetricInstrument() // This validates that we log InstrumentIgnored event // and not something else. - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 33)); + var instrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33); +#if BUILDING_HOSTING_TESTS + // Note: When using IMetricsListener this event is fired twice. Once + // for the SDK listener ignoring it because it isn't listening to + // the meter and then once for IMetricsListener ignoring it because + // decimal is not supported. + Assert.Equal(2, instrumentIgnoredEvents.Count()); +#else + Assert.Single(instrumentIgnoredEvents); +#endif } meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Empty(exportedItems); } - public void Dispose() + internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { - Environment.SetEnvironmentVariable(EmitOverFlowAttributeConfigKey, null); + var configurationData = new Dictionary(); + + if (emitOverflowAttribute) + { + configurationData[EmitOverFlowAttributeConfigKey] = "true"; + } + + if (shouldReclaimUnusedMetricPoints) + { + configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); } private static void CounterUpdateThread(object obj) @@ -1591,10 +1607,10 @@ private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) var metricItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(metricItems) - .Build(); + .AddInMemoryExporter(metricItems)); var argToThread = new UpdateThreadArguments { @@ -1647,10 +1663,10 @@ private void MultithreadedHistogramTest(long[] expected, T[] values) var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddReader(metricReader) - .Build(); + .AddReader(metricReader)); var argsToThread = new UpdateThreadArguments { @@ -1705,7 +1721,7 @@ private class UpdateThreadArguments public class MetricApiTest : MetricApiTestsBase { public MetricApiTest(ITestOutputHelper output) - : base(output, false) + : base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -1713,7 +1729,23 @@ public MetricApiTest(ITestOutputHelper output) public class MetricApiTestWithOverflowAttribute : MetricApiTestsBase { public MetricApiTestWithOverflowAttribute(ITestOutputHelper output) - : base(output, true) + : base(output, emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricApiTestWithReclaimAttribute : MetricApiTestsBase +{ + public MetricApiTestWithReclaimAttribute(ITestOutputHelper output) + : base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class MetricApiTestWithBothOverflowAndReclaimAttributes : MetricApiTestsBase +{ + public MetricApiTestWithBothOverflowAndReclaimAttributes(ITestOutputHelper output) + : base(output, emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index d306e618de3..6529b4ec2c6 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -40,14 +40,14 @@ public void TestExemplarsCounter() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var counter = meter.CreateCounter("testCounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) @@ -94,14 +94,14 @@ public void TestExemplarsHistogram() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) @@ -147,15 +147,15 @@ public void TestExemplarsFilterTags() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs deleted file mode 100644 index e6b1dde373f..00000000000 --- a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs +++ /dev/null @@ -1,447 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics.Metrics; -using System.Reflection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Tests; -using Xunit; - -namespace OpenTelemetry.Metrics.Tests; - -public class MetricOverflowAttributeTests -{ - [Theory] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("FALSE", false)] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("TRUE", true)] - public void TestEmitOverflowAttributeConfigWithEnvVar(string value, bool isEmitOverflowAttributeKeySet) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, value); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; - var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); - - Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("FALSE", false)] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("TRUE", true)] - public void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, bool isEmitOverflowAttributeKeySet) - { - try - { - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value }) - .Build(); - - services.AddSingleton(configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; - var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); - - Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(1, false)] - [InlineData(2, true)] - [InlineData(10, true)] - public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(int maxMetricPoints, bool isEmitOverflowAttributeKeySet) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .SetMaxMetricPointsPerMetricStream(maxMetricPoints) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; - var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); - - Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Delta)] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTemporalityPreference temporalityPreference) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) - .Build(); - - // There are two reserved MetricPoints - // 1. For zero tags - // 2. For metric overflow attribute when user opts-in for this feature - - counter.Add(10); // Record measurement for zero tags - - // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; - - for (int i = 0; i < maxMetricPointsForUse; i++) - { - // Emit unique key-value pairs to use up the available MetricPoints - // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags - counter.Add(10, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint overflowMetricPoint; - - // We still have not exceeded the max MetricPoint limit - Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - exportedItems.Clear(); - metricPoints.Clear(); - - counter.Add(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint zeroTagsMetricPoint; - if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) - { - // Check metric point for zero tags - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - Assert.Equal(10, zeroTagsMetricPoint.GetSumLong()); - } - - // Check metric point for overflow - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); - Assert.Equal(1, overflowMetricPoint.Tags.Count); - Assert.Equal(5, overflowMetricPoint.GetSumLong()); - - exportedItems.Clear(); - metricPoints.Clear(); - - counter.Add(15); // Record another measurement for zero tags - - // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) - { - counter.Add(5, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(15, zeroTagsMetricPoint.GetSumLong()); - - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 - // Number of metric points dropped = 2500 - 1998 = 502 - Assert.Equal(2510, overflowMetricPoint.GetSumLong()); // 502 * 5 - } - else - { - Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); - Assert.Equal(12505, overflowMetricPoint.GetSumLong()); // 5 + (2500 * 5) - } - - exportedItems.Clear(); - metricPoints.Clear(); - - // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred - counter.Add(25); - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); - } - else - { - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - Assert.Equal(50, zeroTagsMetricPoint.GetSumLong()); - Assert.Equal(12505, overflowMetricPoint.GetSumLong()); - } - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Delta)] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderTemporalityPreference temporalityPreference) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("TestHistogram"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) - .Build(); - - // There are two reserved MetricPoints - // 1. For zero tags - // 2. For metric overflow attribute when user opts-in for this feature - - histogram.Record(10); // Record measurement for zero tags - - // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; - - for (int i = 0; i < maxMetricPointsForUse; i++) - { - // Emit unique key-value pairs to use up the available MetricPoints - // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags - histogram.Record(10, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint overflowMetricPoint; - - // We still have not exceeded the max MetricPoint limit - Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - exportedItems.Clear(); - metricPoints.Clear(); - - histogram.Record(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint zeroTagsMetricPoint; - if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) - { - // Check metric point for zero tags - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - Assert.Equal(10, zeroTagsMetricPoint.GetHistogramSum()); - } - - // Check metric point for overflow - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); - Assert.Equal(1, overflowMetricPoint.Tags.Count); - Assert.Equal(5, overflowMetricPoint.GetHistogramSum()); - - exportedItems.Clear(); - metricPoints.Clear(); - - histogram.Record(15); // Record another measurement for zero tags - - // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) - { - histogram.Record(5, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(15, zeroTagsMetricPoint.GetHistogramSum()); - - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 - // Number of metric points dropped = 2500 - 1998 = 502 - Assert.Equal(502, overflowMetricPoint.GetHistogramCount()); - Assert.Equal(2510, overflowMetricPoint.GetHistogramSum()); // 502 * 5 - } - else - { - Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); - - Assert.Equal(2501, overflowMetricPoint.GetHistogramCount()); - Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); // 5 + (2500 * 5) - } - - exportedItems.Clear(); - metricPoints.Clear(); - - // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred - histogram.Record(25); - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); - } - else - { - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - Assert.Equal(50, zeroTagsMetricPoint.GetHistogramSum()); - Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); - } - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } -} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs new file mode 100644 index 00000000000..d275e8c69c2 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs @@ -0,0 +1,489 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics.Metrics; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class MetricOverflowAttributeTestsBase +{ + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + + private readonly bool shouldReclaimUnusedMetricPoints; + private readonly Dictionary configurationData = new() + { + [MetricTestsBase.EmitOverFlowAttributeConfigKey] = "true", + }; + + private readonly IConfiguration configuration; + + public MetricOverflowAttributeTestsBase(bool shouldReclaimUnusedMetricPoints) + { + this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; + + if (shouldReclaimUnusedMetricPoints) + { + this.configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + } + + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(this.configurationData) + .Build(); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestEmitOverflowAttributeConfigWithEnvVar(string value, bool isEmitOverflowAttributeKeySet) + { + // Clear the environment variable value first + Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); + + // Set the environment variable to the value provided in the test input + Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, value); + + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; + var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); + + Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, bool isEmitOverflowAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value }) + .Build(); + + services.AddSingleton(configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; + var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); + + Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); + } + + [Theory] + [InlineData(1, false)] + [InlineData(2, true)] + [InlineData(10, true)] + public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(int maxMetricPoints, bool isEmitOverflowAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .SetMaxMetricPointsPerMetricStream(maxMetricPoints) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore; + var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore); + + Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Delta)] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) + .Build(); + + // There are two reserved MetricPoints + // 1. For zero tags + // 2. For metric overflow attribute when user opts-in for this feature + + counter.Add(10); // Record measurement for zero tags + + // Max number for MetricPoints available for use when emitted with tags + int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; + + for (int i = 0; i < maxMetricPointsForUse; i++) + { + // Emit unique key-value pairs to use up the available MetricPoints + // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags + counter.Add(10, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint overflowMetricPoint; + + // We still have not exceeded the max MetricPoint limit + Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + exportedItems.Clear(); + metricPoints.Clear(); + + counter.Add(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint zeroTagsMetricPoint; + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Check metric point for zero tags + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + Assert.Equal(10, zeroTagsMetricPoint.GetSumLong()); + } + + // Check metric point for overflow + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); + Assert.Equal(1, overflowMetricPoint.Tags.Count); + Assert.Equal(5, overflowMetricPoint.GetSumLong()); + + exportedItems.Clear(); + metricPoints.Clear(); + + counter.Add(15); // Record another measurement for zero tags + + // Emit 2500 more newer MetricPoints with distinct dimension combinations + for (int i = 2000; i < 4500; i++) + { + counter.Add(5, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(15, zeroTagsMetricPoint.GetSumLong()); + + int expectedSum; + + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + if (this.shouldReclaimUnusedMetricPoints) + { + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 + expectedSum = 2510; // 502 * 5 + } + else + { + expectedSum = 12500; // 2500 * 5 + } + + Assert.Equal(expectedSum, overflowMetricPoint.GetSumLong()); + } + else + { + Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); + Assert.Equal(12505, overflowMetricPoint.GetSumLong()); // 5 + (2500 * 5) + } + + exportedItems.Clear(); + metricPoints.Clear(); + + // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred + counter.Add(25); + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); + } + else + { + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + Assert.Equal(50, zeroTagsMetricPoint.GetSumLong()); + Assert.Equal(12505, overflowMetricPoint.GetSumLong()); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Delta)] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("TestHistogram"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) + .Build(); + + // There are two reserved MetricPoints + // 1. For zero tags + // 2. For metric overflow attribute when user opts-in for this feature + + histogram.Record(10); // Record measurement for zero tags + + // Max number for MetricPoints available for use when emitted with tags + int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; + + for (int i = 0; i < maxMetricPointsForUse; i++) + { + // Emit unique key-value pairs to use up the available MetricPoints + // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags + histogram.Record(10, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint overflowMetricPoint; + + // We still have not exceeded the max MetricPoint limit + Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + exportedItems.Clear(); + metricPoints.Clear(); + + histogram.Record(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint zeroTagsMetricPoint; + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Check metric point for zero tags + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + Assert.Equal(10, zeroTagsMetricPoint.GetHistogramSum()); + } + + // Check metric point for overflow + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); + Assert.Equal(1, overflowMetricPoint.Tags.Count); + Assert.Equal(5, overflowMetricPoint.GetHistogramSum()); + + exportedItems.Clear(); + metricPoints.Clear(); + + histogram.Record(15); // Record another measurement for zero tags + + // Emit 2500 more newer MetricPoints with distinct dimension combinations + for (int i = 2000; i < 4500; i++) + { + histogram.Record(5, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(15, zeroTagsMetricPoint.GetHistogramSum()); + + int expectedCount; + int expectedSum; + + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + if (this.shouldReclaimUnusedMetricPoints) + { + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 + expectedCount = 502; + expectedSum = 2510; // 502 * 5 + } + else + { + expectedCount = 2500; + expectedSum = 12500; // 2500 * 5 + } + + Assert.Equal(expectedCount, overflowMetricPoint.GetHistogramCount()); + Assert.Equal(expectedSum, overflowMetricPoint.GetHistogramSum()); + } + else + { + Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); + + Assert.Equal(2501, overflowMetricPoint.GetHistogramCount()); + Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); // 5 + (2500 * 5) + } + + exportedItems.Clear(); + metricPoints.Clear(); + + // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred + histogram.Record(25); + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); + } + else + { + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + Assert.Equal(50, zeroTagsMetricPoint.GetHistogramSum()); + Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); + } + } +} + +public class MetricOverflowAttributeTests : MetricOverflowAttributeTestsBase +{ + public MetricOverflowAttributeTests() + : base(false) + { + } +} + +public class MetricOverflowAttributeTestsWithReclaimAttribute : MetricOverflowAttributeTestsBase +{ + public MetricOverflowAttributeTestsWithReclaimAttribute() + : base(true) + { + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs index b5ef0b65d02..129b1bfbb09 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs @@ -17,6 +17,8 @@ using System.Collections.Concurrent; using System.Diagnostics.Metrics; using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; @@ -24,6 +26,80 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricPointReclaimTests { + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + + private readonly Dictionary configurationData = new() + { + [ReclaimUnusedMetricPointsConfigKey] = "true", + }; + + private readonly IConfiguration configuration; + + public MetricPointReclaimTests() + { + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(this.configurationData) + .Build(); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestReclaimAttributeConfigWithEnvVar(string value, bool isReclaimAttributeKeySet) + { + // Clear the environment variable value first + Environment.SetEnvironmentVariable(ReclaimUnusedMetricPointsConfigKey, null); + + // Set the environment variable to the value provided in the test input + Environment.SetEnvironmentVariable(ReclaimUnusedMetricPointsConfigKey, value); + + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestReclaimAttributeConfigWithOtherConfigProvider(string value, bool isReclaimAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [ReclaimUnusedMetricPointsConfigKey] = value }) + .Build(); + + services.AddSingleton(configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -42,6 +118,10 @@ public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) }; using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(Utils.GetCurrentMethodName()) .AddReader(metricReader) .Build(); @@ -131,6 +211,10 @@ public void MeasurementsAreAggregatedAfterMetricPointReclaim(bool emitMetricWith }; using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(Utils.GetCurrentMethodName()) .SetMaxMetricPointsPerMetricStream(10) // Set max MetricPoints limit to 5 .AddReader(metricReader) @@ -152,12 +236,12 @@ void EmitMetric() { int numberOfMeasurements = 0; var random = new Random(); - while (emitMetricWithNoDimension) + while (true) { if (numberOfMeasurements < numberOfMeasurementsPerThread) { // Check for cases where a metric with no dimension is also emitted - if (true) + if (emitMetricWithNoDimension) { counter.Add(25); Interlocked.Add(ref sum, 25); @@ -196,12 +280,12 @@ void EmitMetric() Assert.Equal(sum, exporter.Sum); } - private class ThreadArguments + private sealed class ThreadArguments { public int Counter; } - private class CustomExporter : BaseExporter + private sealed class CustomExporter : BaseExporter { public long Sum = 0; diff --git a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs index dc0810fd338..9a22f7b6795 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs @@ -15,7 +15,8 @@ // using System.Diagnostics.Metrics; - +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; @@ -24,14 +25,15 @@ namespace OpenTelemetry.Metrics.Tests; #pragma warning disable SA1402 -public abstract class MetricSnapshotTestsBase : IDisposable +public abstract class MetricSnapshotTestsBase { - protected MetricSnapshotTestsBase(bool emitOverflowAttribute) + private readonly IConfiguration configuration; + + protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { - if (emitOverflowAttribute) - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - } + this.configuration = MetricApiTestsBase.BuildConfiguration( + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints); } [Fact] @@ -43,6 +45,10 @@ public void VerifySnapshot_Counter() using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("meter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -112,6 +118,10 @@ public void VerifySnapshot_Histogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -204,6 +214,10 @@ public void VerifySnapshot_ExponentialHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddView("histogram", new Base2ExponentialBucketHistogramConfiguration()) .AddInMemoryExporter(exportedMetrics) @@ -292,17 +306,12 @@ public void VerifySnapshot_ExponentialHistogram() Assert.Equal(10, max); AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData()); } - - public void Dispose() - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } } public class MetricSnapshotTests : MetricSnapshotTestsBase { public MetricSnapshotTests() - : base(false) + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -310,7 +319,23 @@ public MetricSnapshotTests() public class MetricSnapshotTestsWithOverflowAttribute : MetricSnapshotTestsBase { public MetricSnapshotTestsWithOverflowAttribute() - : base(true) + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricSnapshotTestsWithReclaimAttribute : MetricSnapshotTestsBase +{ + public MetricSnapshotTestsWithReclaimAttribute() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class MetricSnapshotTestsWithBothAttributes : MetricSnapshotTestsBase +{ + public MetricSnapshotTestsWithBothAttributes() + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 6cb5395ed29..df8d4ecac73 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -14,6 +14,15 @@ // limitations under the License. // +#if BUILDING_HOSTING_TESTS +using System.Diagnostics; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#if BUILDING_HOSTING_TESTS +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Hosting; +#endif using Xunit; namespace OpenTelemetry.Metrics.Tests; @@ -21,6 +30,76 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricTestsBase { public const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + + protected readonly IConfiguration configuration; + + protected MetricTestsBase() + { + } + + protected MetricTestsBase(IConfiguration configuration) + { + this.configuration = configuration; + } + +#if BUILDING_HOSTING_TESTS + public static IHost BuildHost( + bool useWithMetricsStyle, + Action configureAppConfiguration = null, + Action configureServices = null, + Action configureMetricsBuilder = null, + Action configureMeterProviderBuilder = null) + { + var hostBuilder = new HostBuilder() + .ConfigureDefaults(null) + .ConfigureAppConfiguration((context, builder) => + { + configureAppConfiguration?.Invoke(context, builder); + }) + .ConfigureServices(services => + { + configureServices?.Invoke(services); + + services.AddMetrics(builder => + { + configureMetricsBuilder?.Invoke(builder); + + if (!useWithMetricsStyle) + { + builder.UseOpenTelemetry(metricsBuilder => ConfigureBuilder(metricsBuilder, configureMeterProviderBuilder)); + } + }); + + if (useWithMetricsStyle) + { + services + .AddOpenTelemetry() + .WithMetrics(metricsBuilder => ConfigureBuilder(metricsBuilder, configureMeterProviderBuilder)); + } + + services.AddHostedService(); + }); + + var host = hostBuilder.Build(); + + host.Start(); + + return host; + + static void ConfigureBuilder(MeterProviderBuilder builder, Action configureMeterProviderBuilder) + { + IServiceCollection localServices = null; + + builder.ConfigureServices(services => localServices = services); + + Debug.Assert(localServices != null, "localServices was null"); + + var testBuilder = new HostingMeterProviderBuilder(localServices); + configureMeterProviderBuilder?.Invoke(testBuilder); + } + } +#endif // This method relies on the assumption that MetricPoints are exported in the order in which they are emitted. // For Delta AggregationTemporality, this holds true only until the AggregatorStore has not begun recaliming the MetricPoints. @@ -129,8 +208,106 @@ public static void CheckTagsForNthMetricPoint(List metrics, List configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + +#if BUILDING_HOSTING_TESTS + var host = BuildHost( + useWithMetricsStyle: false, + configureMeterProviderBuilder: configure, + configureServices: services => + { + if (this.configuration != null) + { + services.AddSingleton(this.configuration); + } + }); + + meterProvider = host.Services.GetService(); + + return host; +#else + var builder = Sdk.CreateMeterProviderBuilder(); + + if (this.configuration != null) + { + builder.ConfigureServices(services => services.AddSingleton(this.configuration)); + } + + configure(builder); + + return meterProvider = builder.Build(); +#endif + } + internal static Exemplar[] GetExemplars(MetricPoint mp) { return mp.GetExemplars().Where(exemplar => exemplar.Timestamp != default).ToArray(); } + +#if BUILDING_HOSTING_TESTS + public sealed class HostingMeterProviderBuilder : MeterProviderBuilderBase + { + public HostingMeterProviderBuilder(IServiceCollection services) + : base(services) + { + } + + public override MeterProviderBuilder AddMeter(params string[] names) + { + return this.ConfigureServices(services => + { + foreach (var name in names) + { + // Note: The entire purpose of this class is to use the + // IMetricsBuilder API to enable Metrics and NOT the + // traditional AddMeter API. + services.AddMetrics(builder => builder.EnableMetrics(name)); + } + }); + } + + public MeterProviderBuilder AddSdkMeter(params string[] names) + { + return base.AddMeter(names); + } + } + + private sealed class MetricsSubscriptionManagerCleanupHostedService : IHostedService, IDisposable + { + private readonly object metricsSubscriptionManager; + + public MetricsSubscriptionManagerCleanupHostedService(IServiceProvider serviceProvider) + { + this.metricsSubscriptionManager = serviceProvider.GetService( + typeof(ConsoleMetrics).Assembly.GetType("Microsoft.Extensions.Diagnostics.Metrics.MetricsSubscriptionManager")); + + if (this.metricsSubscriptionManager == null) + { + throw new InvalidOperationException("MetricsSubscriptionManager could not be found reflectively."); + } + } + + public void Dispose() + { + // Note: The current version of MetricsSubscriptionManager seems to + // be bugged in that it doesn't implement IDisposable. This hack + // manually invokes Dispose so that tests don't clobber each other. + // See: https://github.com/dotnet/runtime/issues/94434. + this.metricsSubscriptionManager.GetType().GetMethod("Dispose").Invoke(this.metricsSubscriptionManager, null); + } + + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + } +#endif } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index df98fc6cc2a..049c972fd90 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -30,11 +30,11 @@ public void ViewToRenameMetric() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamed") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterLong = meter.CreateCounter("name1"); @@ -53,19 +53,17 @@ public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); - ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", new MetricStreamConfiguration() { Name = viewNewName }) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); } @@ -77,11 +75,10 @@ public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException( using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", (MetricStreamConfiguration)null) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -91,11 +88,10 @@ public void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", name: "newname") - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -105,11 +101,10 @@ public void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExcep using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -118,17 +113,18 @@ public void AddViewWithExceptionInUserCallbackAppliedDefault() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { var counter1 = meter1.CreateCounter("counter1"); counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -144,12 +140,11 @@ public void AddViewWithExceptionInUserCallbackNoDefault() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) .AddView("*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { @@ -172,18 +167,19 @@ public void AddViewsWithAndWithoutExceptionInUserCallback() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) .AddView((instrument) => { return new MetricStreamConfiguration() { Name = "newname" }; }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { var counter1 = meter1.CreateCounter("counter1"); counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -198,8 +194,8 @@ public void AddViewsWithAndWithoutExceptionInUserCallback() [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries }))); Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); } @@ -210,8 +206,8 @@ public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] bo [InlineData(1)] public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize }))); Assert.Contains("Histogram max size is invalid", ex.Message); } @@ -221,8 +217,8 @@ public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentExc [InlineData(21)] public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale }))); Assert.Contains("Histogram max scale is invalid", ex.Message); } @@ -237,7 +233,7 @@ public void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) var counter1 = meter1.CreateCounter("counter1"); - using (var provider = Sdk.CreateMeterProviderBuilder() + using (var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -245,8 +241,7 @@ public void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) ? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries } : null; }) - .AddInMemoryExporter(exportedItems) - .Build()) + .AddInMemoryExporter(exportedItems))) { counter1.Add(1); } @@ -263,11 +258,10 @@ public void ViewWithValidNameExported(string viewNewName) var exportedItems = new List(); using var meter1 = new Meter("ViewWithInvalidNameIgnoredTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter1.CreateCounter("name1"); counterLong.Add(10); @@ -287,7 +281,7 @@ public void ViewToRenameMetricConditionally() var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddView((instrument) => @@ -302,8 +296,7 @@ public void ViewToRenameMetricConditionally() return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Without views only 1 stream would be // exported (the 2nd one gets dropped due to @@ -327,7 +320,7 @@ public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) // since here it's a func, we can't validate the name right away @@ -345,8 +338,7 @@ public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Because the MetricStreamName passed is invalid, the view is ignored, // and default aggregation is used. @@ -364,7 +356,7 @@ public void ViewWithValidNameConditionally(string viewNewName) { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -379,8 +371,7 @@ public void ViewWithValidNameConditionally(string viewNewName) return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); @@ -401,7 +392,7 @@ public void ViewWithNullCustomNameTakesInstrumentName() using var meter = new Meter("ViewToRenameMetricConditionallyTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -415,8 +406,7 @@ public void ViewWithNullCustomNameTakesInstrumentName() return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. // Since the View name was null, the instrument name was used instead @@ -436,12 +426,12 @@ public void ViewToProduceMultipleStreamsFromInstrument() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting two metric stream. var counterLong = meter.CreateCounter("name1"); @@ -457,13 +447,13 @@ public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting three metric stream. // the second .AddView("name1", "renamedStream2") @@ -482,12 +472,12 @@ public void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("NotAHistogram", new ExplicitBucketHistogramConfiguration() { Name = "ImAnExplicitBoundsHistogram" }) .AddView("NotAHistogram", new Base2ExponentialBucketHistogramConfiguration() { Name = "ImAnExponentialHistogram" }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("NotAHistogram"); counter.Add(10); @@ -515,12 +505,12 @@ public void ViewToProduceCustomHistogramBound() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); var boundaries = new double[] { 10, 20 }; - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Name = "MyHistogramDefaultBound" }) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var histogram = meter.CreateHistogram("MyHistogram"); histogram.Record(-10); @@ -600,11 +590,11 @@ public void ViewToProduceExponentialHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new Base2ExponentialBucketHistogramConfiguration()) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var histogram = meter.CreateHistogram("MyHistogram"); var expectedHistogram = new Base2ExponentialBucketHistogram(); @@ -648,11 +638,11 @@ public void HistogramMinMax(double[] values, HistogramConfiguration histogramCon using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); for (var i = 0; i < values.Length; i++) { @@ -686,11 +676,11 @@ public void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration hi using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); for (var i = 0; i < values.Length; i++) { @@ -714,7 +704,8 @@ public void ViewToSelectTagKeys() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("FruitCounter", new MetricStreamConfiguration() { @@ -731,8 +722,7 @@ public void ViewToSelectTagKeys() TagKeys = Array.Empty(), Name = "NoTags", }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("FruitCounter"); counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); @@ -785,11 +775,11 @@ public void ViewToDropSingleInstrument() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("counterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterInteresting = meter.CreateCounter("counterInteresting"); @@ -808,11 +798,11 @@ public void ViewToDropSingleInstrumentObservableCounter() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. meter.CreateObservableCounter("observableCounterNotInteresting", () => { return 10; }, "ms"); @@ -829,11 +819,11 @@ public void ViewToDropSingleInstrumentObservableGauge() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. meter.CreateObservableGauge("observableGaugeNotInteresting", () => { return 10; }, "ms"); @@ -850,11 +840,11 @@ public void ViewToDropMultipleInstruments() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting two client metric streams as both server* are dropped. var serverRequests = meter.CreateCounter("server.requests"); @@ -877,12 +867,12 @@ public void ViewToDropAndRetainInstrument() { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server.requests", MetricStreamConfiguration.Drop) .AddView("server.requests", "server.request_renamed") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream even though a View is asking // to drop the instrument, because another View is matching @@ -902,13 +892,12 @@ public void ViewConflict_OneInstrument_DifferentDescription() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription1" }) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription2" }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -949,7 +938,8 @@ public void ViewConflict_TwoDistinctInstruments_ThreeStreams() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -961,9 +951,7 @@ public void ViewConflict_TwoDistinctInstruments_ThreeStreams() ? new MetricStreamConfiguration() { Name = "MetricStreamB" } : new MetricStreamConfiguration() { Name = "MetricStreamC" }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name", "unit", "description1"); var instrument2 = meter.CreateCounter("name", "unit", "description2"); @@ -1006,7 +994,8 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1016,9 +1005,7 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() { return new MetricStreamConfiguration { TagKeys = new[] { "key2" } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); @@ -1054,7 +1041,8 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1064,9 +1052,7 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() { return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); @@ -1103,7 +1089,8 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1113,9 +1100,7 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun { return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateHistogram("name"); var instrument2 = meter.CreateHistogram("name"); @@ -1181,7 +1166,8 @@ public void ViewConflict_TwoInstruments_OneMatchesView() var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1194,9 +1180,7 @@ public void ViewConflict_TwoInstruments_OneMatchesView() return null; } }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername"); @@ -1235,7 +1219,8 @@ public void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIs var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1248,9 +1233,7 @@ public void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIs return MetricStreamConfiguration.Drop; } }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername"); diff --git a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj index d8ad801f561..e7b5c8a03e7 100644 --- a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj +++ b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj @@ -36,5 +36,6 @@ runtime; build; native; contentfiles; analyzers + diff --git a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs index b59062f1042..7c69b4a455e 100644 --- a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs +++ b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs @@ -76,7 +76,7 @@ public RunningServer(Action action, string host, int port) this.initialized.Set(); - action(await ctxTask.ConfigureAwait(false)); + action(await ctxTask); } catch (Exception ex) { diff --git a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs b/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs index ad49fb7e233..6c839b4f061 100644 --- a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs +++ b/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs @@ -74,7 +74,7 @@ await Task.Factory.StartNew(() => Assert.False(Sdk.SuppressInstrumentation); Assert.Equal(1, SuppressInstrumentationScope.Enter()); Assert.True(Sdk.SuppressInstrumentation); - }).ConfigureAwait(false); + }); Assert.False(Sdk.SuppressInstrumentation); // Changes made by SuppressInstrumentationScope.Enter in the task above are not reflected here as it's not part of the same async flow } diff --git a/test/TestApp.AspNetCore/ActivityMiddleware.cs b/test/TestApp.AspNetCore/ActivityMiddleware.cs index 3fa497a878e..2da1225c3b0 100644 --- a/test/TestApp.AspNetCore/ActivityMiddleware.cs +++ b/test/TestApp.AspNetCore/ActivityMiddleware.cs @@ -34,7 +34,7 @@ public async Task InvokeAsync(HttpContext context) this.impl.PreProcess(context); } - await this.next(context).ConfigureAwait(false); + await this.next(context); if (this.impl != null) { diff --git a/test/TestApp.AspNetCore/CallbackMiddleware.cs b/test/TestApp.AspNetCore/CallbackMiddleware.cs index 2f6a4085859..0a79505a5bd 100644 --- a/test/TestApp.AspNetCore/CallbackMiddleware.cs +++ b/test/TestApp.AspNetCore/CallbackMiddleware.cs @@ -29,9 +29,9 @@ public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) public async Task InvokeAsync(HttpContext context) { - if (this.impl == null || await this.impl.ProcessAsync(context).ConfigureAwait(false)) + if (this.impl == null || await this.impl.ProcessAsync(context)) { - await this.next(context).ConfigureAwait(false); + await this.next(context); } } @@ -39,7 +39,7 @@ public class CallbackMiddlewareImpl { public virtual async Task ProcessAsync(HttpContext context) { - return await Task.FromResult(true).ConfigureAwait(false); + return await Task.FromResult(true); } } }